OSDN Git Service

am 771d1471: Merge commit \'remotes/goog/eclair\' into eclair-release
[android-x86/packages-apps-Contacts.git] / src / com / android / contacts / ui / QuickContactWindow.java
1 /*
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
17 package com.android.contacts.ui;
18
19 import com.android.contacts.ContactsUtils;
20 import com.android.contacts.R;
21 import com.android.contacts.model.ContactsSource;
22 import com.android.contacts.model.Sources;
23 import com.android.contacts.model.ContactsSource.DataKind;
24 import com.android.contacts.ui.widget.CheckableImageView;
25 import com.android.contacts.util.Constants;
26 import com.android.contacts.util.NotifyingAsyncQueryHandler;
27 import com.android.internal.policy.PolicyManager;
28 import com.google.android.collect.Lists;
29 import com.google.android.collect.Sets;
30
31 import android.content.ActivityNotFoundException;
32 import android.content.ContentValues;
33 import android.content.ContentUris;
34 import android.content.Context;
35 import android.content.EntityIterator;
36 import android.content.Intent;
37 import android.content.pm.ApplicationInfo;
38 import android.content.pm.PackageManager;
39 import android.content.pm.ResolveInfo;
40 import android.content.res.Resources;
41 import android.database.Cursor;
42 import android.graphics.Bitmap;
43 import android.graphics.BitmapFactory;
44 import android.graphics.Rect;
45 import android.graphics.drawable.Drawable;
46 import android.net.Uri;
47 import android.os.Handler;
48 import android.provider.ContactsContract.Contacts;
49 import android.provider.ContactsContract.Data;
50 import android.provider.ContactsContract.QuickContact;
51 import android.provider.ContactsContract.RawContacts;
52 import android.provider.ContactsContract.StatusUpdates;
53 import android.provider.ContactsContract.CommonDataKinds.Email;
54 import android.provider.ContactsContract.CommonDataKinds.Im;
55 import android.provider.ContactsContract.CommonDataKinds.Phone;
56 import android.provider.ContactsContract.CommonDataKinds.Photo;
57 import android.provider.SocialContract.Activities;
58 import android.text.TextUtils;
59 import android.text.format.DateUtils;
60 import android.util.Log;
61 import android.util.Pool;
62 import android.util.Poolable;
63 import android.util.PoolableManager;
64 import android.util.Pools;
65 import android.view.ContextThemeWrapper;
66 import android.view.Gravity;
67 import android.view.KeyEvent;
68 import android.view.LayoutInflater;
69 import android.view.Menu;
70 import android.view.MenuItem;
71 import android.view.MotionEvent;
72 import android.view.VelocityTracker;
73 import android.view.View;
74 import android.view.ViewGroup;
75 import android.view.ViewStub;
76 import android.view.Window;
77 import android.view.WindowManager;
78 import android.view.Window.Callback;
79 import android.view.accessibility.AccessibilityEvent;
80 import android.view.animation.Animation;
81 import android.view.animation.AnimationUtils;
82 import android.view.animation.Interpolator;
83 import android.widget.AbsListView;
84 import android.widget.AdapterView;
85 import android.widget.BaseAdapter;
86 import android.widget.CheckBox;
87 import android.widget.CompoundButton;
88 import android.widget.HorizontalScrollView;
89 import android.widget.ImageView;
90 import android.widget.ListView;
91 import android.widget.TextView;
92 import android.widget.Toast;
93
94 import java.lang.ref.SoftReference;
95 import java.util.ArrayList;
96 import java.util.Arrays;
97 import java.util.HashMap;
98 import java.util.HashSet;
99 import java.util.LinkedList;
100 import java.util.List;
101 import java.util.Set;
102
103 /**
104  * Window that shows QuickContact dialog for a specific {@link Contacts#_ID}.
105  */
106 public class QuickContactWindow implements Window.Callback,
107         NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener,
108         AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback {
109     private static final String TAG = "QuickContactWindow";
110
111     /**
112      * Interface used to allow the person showing a {@link QuickContactWindow} to
113      * know when the window has been dismissed.
114      */
115     public interface OnDismissListener {
116         public void onDismiss(QuickContactWindow dialog);
117     }
118
119     private final Context mContext;
120     private final LayoutInflater mInflater;
121     private final WindowManager mWindowManager;
122     private Window mWindow;
123     private View mDecor;
124     private final Rect mRect = new Rect();
125
126     private boolean mQuerying = false;
127     private boolean mShowing = false;
128
129     private NotifyingAsyncQueryHandler mHandler;
130     private OnDismissListener mDismissListener;
131     private ResolveCache mResolveCache;
132
133     private Uri mLookupUri;
134     private Rect mAnchor;
135
136     private int mShadowHeight;
137
138     private boolean mHasValidSocial = false;
139     private boolean mHasData = false;
140     private boolean mMakePrimary = false;
141
142     private ImageView mArrowUp;
143     private ImageView mArrowDown;
144
145     private int mMode;
146     private View mHeader;
147     private HorizontalScrollView mTrackScroll;
148     private ViewGroup mTrack;
149     private Animation mTrackAnim;
150
151     private View mFooter;
152     private View mFooterDisambig;
153     private ListView mResolveList;
154     private CheckableImageView mLastAction;
155     private CheckBox mSetPrimaryCheckBox;
156
157     private int mWindowRecycled = 0;
158     private int mActionRecycled = 0;
159
160     /**
161      * Set of {@link Action} that are associated with the aggregate currently
162      * displayed by this dialog, represented as a map from {@link String}
163      * MIME-type to {@link ActionList}.
164      */
165     private ActionMap mActions = new ActionMap();
166
167     /**
168      * Pool of unused {@link CheckableImageView} that have previously been
169      * inflated, and are ready to be recycled through {@link #obtainView()}.
170      */
171     private LinkedList<View> mActionPool = new LinkedList<View>();
172
173     private String[] mExcludeMimes;
174
175     /**
176      * Specific MIME-types that should be bumped to the front of the dialog.
177      * Other MIME-types not appearing in this list follow in alphabetic order.
178      */
179     private static final String[] ORDERED_MIMETYPES = new String[] {
180             Phone.CONTENT_ITEM_TYPE,
181             Contacts.CONTENT_ITEM_TYPE,
182             Constants.MIME_SMS_ADDRESS,
183             Email.CONTENT_ITEM_TYPE,
184     };
185
186     /**
187      * Specific list {@link ApplicationInfo#packageName} of apps that are
188      * prefered <strong>only</strong> for the purposes of default icons when
189      * multiple {@link ResolveInfo} are found to match. This only happens when
190      * the user has not selected a default app yet, and they will still be
191      * presented with the system disambiguation dialog.
192      */
193     private static final HashSet<String> sPreferResolve = Sets.newHashSet(new String[] {
194             "com.android.email",
195             "com.android.calendar",
196             "com.android.contacts",
197             "com.android.mms",
198             "com.android.phone",
199     });
200
201     private static final int TOKEN_DATA = 1;
202
203     static final boolean LOGD = false;
204
205     static final boolean TRACE_LAUNCH = false;
206     static final String TRACE_TAG = "quickcontact";
207
208     /**
209      * Prepare a dialog to show in the given {@link Context}.
210      */
211     public QuickContactWindow(Context context) {
212         mContext = new ContextThemeWrapper(context, R.style.QuickContact);
213         mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
214         mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
215
216         mWindow = PolicyManager.makeNewWindow(mContext);
217         mWindow.setCallback(this);
218         mWindow.setWindowManager(mWindowManager, null, null);
219
220         mWindow.setContentView(R.layout.quickcontact);
221
222         mArrowUp = (ImageView)mWindow.findViewById(R.id.arrow_up);
223         mArrowDown = (ImageView)mWindow.findViewById(R.id.arrow_down);
224
225         mResolveCache = new ResolveCache(mContext);
226
227         final Resources res = mContext.getResources();
228         mShadowHeight = res.getDimensionPixelSize(R.dimen.quickcontact_shadow);
229
230         mTrack = (ViewGroup)mWindow.findViewById(R.id.quickcontact);
231         mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
232
233         mFooter = mWindow.findViewById(R.id.footer);
234         mFooterDisambig = mWindow.findViewById(R.id.footer_disambig);
235         mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
236         mSetPrimaryCheckBox = (CheckBox)mWindow.findViewById(android.R.id.checkbox);
237
238         mSetPrimaryCheckBox.setOnCheckedChangeListener(this);
239
240         // Prepare track entrance animation
241         mTrackAnim = AnimationUtils.loadAnimation(mContext, R.anim.quickcontact);
242         mTrackAnim.setInterpolator(new Interpolator() {
243             public float getInterpolation(float t) {
244                 // Pushes past the target area, then snaps back into place.
245                 // Equation for graphing: 1.2-((x*1.6)-1.1)^2
246                 final float inner = (t * 1.55f) - 1.1f;
247                 return 1.2f - inner * inner;
248             }
249         });
250
251         mHandler = new NotifyingAsyncQueryHandler(mContext, this);
252     }
253
254     /**
255      * Prepare a dialog to show in the given {@link Context}, and notify the
256      * given {@link OnDismissListener} each time this dialog is dismissed.
257      */
258     public QuickContactWindow(Context context, OnDismissListener dismissListener) {
259         this(context);
260         mDismissListener = dismissListener;
261     }
262
263     private View getHeaderView(int mode) {
264         View header = null;
265         switch (mode) {
266             case QuickContact.MODE_SMALL:
267                 header = mWindow.findViewById(R.id.header_small);
268                 break;
269             case QuickContact.MODE_MEDIUM:
270                 header = mWindow.findViewById(R.id.header_medium);
271                 break;
272             case QuickContact.MODE_LARGE:
273                 header = mWindow.findViewById(R.id.header_large);
274                 break;
275         }
276
277         if (header instanceof ViewStub) {
278             // Inflate actual header if we picked a stub
279             final ViewStub stub = (ViewStub)header;
280             header = stub.inflate();
281         } else {
282             header.setVisibility(View.VISIBLE);
283         }
284
285         return header;
286     }
287
288     /**
289      * Start showing a dialog for the given {@link Contacts#_ID} pointing
290      * towards the given location.
291      */
292     public void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
293         if (mShowing || mQuerying) {
294             Log.w(TAG, "already in process of showing");
295             return;
296         }
297
298         if (TRACE_LAUNCH && !android.os.Debug.isMethodTracingActive()) {
299             android.os.Debug.startMethodTracing(TRACE_TAG);
300         }
301
302         // Prepare header view for requested mode
303         mLookupUri = lookupUri;
304         mAnchor = new Rect(anchor);
305         mMode = mode;
306         mExcludeMimes = excludeMimes;
307
308         mHeader = getHeaderView(mode);
309
310         setHeaderText(R.id.name, R.string.quickcontact_missing_name);
311
312         setHeaderText(R.id.status, null);
313         setHeaderText(R.id.timestamp, null);
314
315         setHeaderImage(R.id.presence, null);
316         setHeaderImage(R.id.source, null);
317
318         mHasValidSocial = false;
319         mQuerying = true;
320
321         // Start background query for data, but only select photo rows when they
322         // directly match the super-primary PHOTO_ID.
323         final Uri dataUri = getDataUri(lookupUri);
324         mHandler.cancelOperation(TOKEN_DATA);
325
326         // Only request photo data when required by mode
327         if (mMode == QuickContact.MODE_LARGE) {
328             // Select photos, but only super-primary
329             mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
330                     + "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
331                     + ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
332         } else {
333             // Exclude all photos from cursor
334             mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
335                     + "!=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
336         }
337     }
338
339     /**
340      * Build a {@link Uri} into the {@link Data} table for the requested
341      * {@link Contacts#CONTENT_LOOKUP_URI} style {@link Uri}.
342      */
343     private Uri getDataUri(Uri lookupUri) {
344         // TODO: Formalize method of extracting LOOKUP_KEY
345         final List<String> path = lookupUri.getPathSegments();
346         final boolean validLookup = path.size() >= 3 && "lookup".equals(path.get(1));
347         if (!validLookup) {
348             // We only accept valid lookup-style Uris
349             throw new IllegalArgumentException("Expecting lookup-style Uri");
350         } else if (path.size() == 3) {
351             // No direct _ID provided, so force a lookup
352             lookupUri = Contacts.lookupContact(mContext.getContentResolver(), lookupUri);
353         }
354
355         final long contactId = ContentUris.parseId(lookupUri);
356         return Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
357                 Contacts.Data.CONTENT_DIRECTORY);
358     }
359
360     /**
361      * Show the correct call-out arrow based on a {@link R.id} reference.
362      */
363     private void showArrow(int whichArrow, int requestedX) {
364         final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
365         final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
366
367         final int arrowWidth = mArrowUp.getMeasuredWidth();
368
369         showArrow.setVisibility(View.VISIBLE);
370         ViewGroup.MarginLayoutParams param = (ViewGroup.MarginLayoutParams)showArrow.getLayoutParams();
371         param.leftMargin = requestedX - arrowWidth / 2;
372
373         hideArrow.setVisibility(View.INVISIBLE);
374     }
375
376     /**
377      * Actual internal method to show this dialog. Called only by
378      * {@link #considerShowing()} when all data requirements have been met.
379      */
380     private void showInternal() {
381         mDecor = mWindow.getDecorView();
382         WindowManager.LayoutParams l = mWindow.getAttributes();
383
384         l.width = WindowManager.LayoutParams.FILL_PARENT;
385         l.height = WindowManager.LayoutParams.WRAP_CONTENT;
386
387         // Force layout measuring pass so we have baseline numbers
388         mDecor.measure(l.width, l.height);
389
390         final int blockHeight = mDecor.getMeasuredHeight();
391
392         l.gravity = Gravity.TOP | Gravity.LEFT;
393         l.x = 0;
394
395         if (mAnchor.top > blockHeight) {
396             // Show downwards callout when enough room, aligning bottom block
397             // edge with top of anchor area, and adjusting to inset arrow.
398             showArrow(R.id.arrow_down, mAnchor.centerX());
399             l.y = mAnchor.top - blockHeight + mShadowHeight;
400             l.windowAnimations = R.style.QuickContactAboveAnimation;
401
402         } else {
403             // Otherwise show upwards callout, aligning block top with bottom of
404             // anchor area, and adjusting to inset arrow.
405             showArrow(R.id.arrow_up, mAnchor.centerX());
406             l.y = mAnchor.bottom - mShadowHeight;
407             l.windowAnimations = R.style.QuickContactBelowAnimation;
408
409         }
410
411         l.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
412                 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
413
414         mWindowManager.addView(mDecor, l);
415         mShowing = true;
416         mQuerying = false;
417
418         mTrack.startAnimation(mTrackAnim);
419
420         if (TRACE_LAUNCH) {
421             android.os.Debug.stopMethodTracing();
422             Log.d(TAG, "Window recycled " + mWindowRecycled + " times, chiclets "
423                     + mActionRecycled + " times");
424         }
425     }
426
427     /**
428      * Dismiss this dialog if showing.
429      */
430     public void dismiss() {
431         // Notify any listeners that we've been dismissed
432         if (mDismissListener != null) {
433             mDismissListener.onDismiss(this);
434         }
435
436         if (!isShowing()) {
437             if (LOGD) Log.d(TAG, "not visible, ignore");
438             return;
439         }
440
441         boolean hadDecor = mDecor != null;
442         if (hadDecor) {
443             mWindowManager.removeView(mDecor);
444             mDecor = null;
445             mWindow.closeAllPanels();
446         }
447
448         // Release reference to last chiclet.
449         mLastAction = null;
450
451         // Completely hide header from current mode
452         mHeader.setVisibility(View.GONE);
453
454         // Cancel any pending queries
455         mHandler.cancelOperation(TOKEN_DATA);
456
457         // Clear track actions and scroll to hard left
458         mResolveCache.clear();
459         mActions.clear();
460
461         // Recycle any chiclets in use
462         while (mTrack.getChildCount() > 2) {
463             this.releaseView(mTrack.getChildAt(1));
464             mTrack.removeViewAt(1);
465         }
466
467         mTrackScroll.fullScroll(View.FOCUS_LEFT);
468         mWasDownArrow = false;
469
470         setResolveVisible(false, null);
471
472         mQuerying = false;
473
474         if (!hadDecor || !mShowing) {
475             if (LOGD) Log.d(TAG, "not showing, ignore");
476             return;
477         }
478
479         mShowing = false;
480         mWindowRecycled++;
481     }
482
483     /**
484      * Returns true if this dialog is showing or querying.
485      */
486     public boolean isShowing() {
487         return mShowing || mQuerying;
488     }
489
490     /**
491      * Consider showing this window, which will only call through to
492      * {@link #showInternal()} when all data items are present.
493      */
494     private synchronized void considerShowing() {
495         if (mHasData && !mShowing) {
496             if (mMode == QuickContact.MODE_MEDIUM && !mHasValidSocial) {
497                 // Missing valid social, swap medium for small header
498                 mHeader.setVisibility(View.GONE);
499                 mHeader = getHeaderView(QuickContact.MODE_SMALL);
500             }
501
502             // All queries have returned, pull curtain
503             showInternal();
504         }
505     }
506
507     /** {@inheritDoc} */
508     public void onQueryComplete(int token, Object cookie, Cursor cursor) {
509         if (cursor == null) {
510             // Problem while running query, so bail without showing
511             Log.w(TAG, "Missing cursor for token=" + token);
512             this.dismiss();
513             return;
514         }
515
516         handleData(cursor);
517         mHasData = true;
518
519         if (!cursor.isClosed()) {
520             cursor.close();
521         }
522
523         considerShowing();
524     }
525
526     /** Assign this string to the view, if found in {@link #mHeader}. */
527     private void setHeaderText(int id, int resId) {
528         setHeaderText(id, mContext.getResources().getText(resId));
529     }
530
531     /** Assign this string to the view, if found in {@link #mHeader}. */
532     private void setHeaderText(int id, CharSequence value) {
533         final View view = mHeader.findViewById(id);
534         if (view instanceof TextView) {
535             ((TextView)view).setText(value);
536             view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE);
537         }
538     }
539
540     /** Assign this image to the view, if found in {@link #mHeader}. */
541     private void setHeaderImage(int id, int resId) {
542         setHeaderImage(id, mContext.getResources().getDrawable(resId));
543     }
544
545     /** Assign this image to the view, if found in {@link #mHeader}. */
546     private void setHeaderImage(int id, Drawable drawable) {
547         final View view = mHeader.findViewById(id);
548         if (view instanceof ImageView) {
549             ((ImageView)view).setImageDrawable(drawable);
550             view.setVisibility(drawable == null ? View.GONE : View.VISIBLE);
551         }
552     }
553
554     /**
555      * Find the presence icon for showing in summary header.
556      */
557     private Drawable getPresenceIcon(int status) {
558         int resId = -1;
559         switch (status) {
560             case StatusUpdates.AVAILABLE:
561                 resId = android.R.drawable.presence_online;
562                 break;
563             case StatusUpdates.IDLE:
564             case StatusUpdates.AWAY:
565                 resId = android.R.drawable.presence_away;
566                 break;
567             case StatusUpdates.DO_NOT_DISTURB:
568                 resId = android.R.drawable.presence_busy;
569                 break;
570         }
571         if (resId != -1) {
572             return mContext.getResources().getDrawable(resId);
573         } else {
574             return null;
575         }
576     }
577
578     /**
579      * Find the QuickContact-specific presence icon for showing in chiclets.
580      */
581     private Drawable getTrackPresenceIcon(int status) {
582         int resId = -1;
583         switch (status) {
584             case StatusUpdates.AVAILABLE:
585                 resId = R.drawable.quickcontact_slider_presence_active;
586                 break;
587             case StatusUpdates.IDLE:
588             case StatusUpdates.AWAY:
589                 resId = R.drawable.quickcontact_slider_presence_away;
590                 break;
591             case StatusUpdates.DO_NOT_DISTURB:
592                 resId = R.drawable.quickcontact_slider_presence_busy;
593                 break;
594             case StatusUpdates.INVISIBLE:
595                 resId = R.drawable.quickcontact_slider_presence_inactive;
596                 break;
597             case StatusUpdates.OFFLINE:
598             default:
599                 resId = R.drawable.quickcontact_slider_presence_inactive;
600         }
601         return mContext.getResources().getDrawable(resId);
602     }
603
604     /** Read {@link String} from the given {@link Cursor}. */
605     private static String getAsString(Cursor cursor, String columnName) {
606         final int index = cursor.getColumnIndex(columnName);
607         return cursor.getString(index);
608     }
609
610     /** Read {@link Integer} from the given {@link Cursor}. */
611     private static int getAsInt(Cursor cursor, String columnName) {
612         final int index = cursor.getColumnIndex(columnName);
613         return cursor.getInt(index);
614     }
615
616     /**
617      * Abstract definition of an action that could be performed, along with
618      * string description and icon.
619      */
620     private interface Action {
621         public CharSequence getHeader();
622         public CharSequence getBody();
623
624         public String getMimeType();
625         public Drawable getFallbackIcon();
626
627         /**
628          * Build an {@link Intent} that will perform this action.
629          */
630         public Intent getIntent();
631
632         /**
633          * Checks if the contact data for this action is primary.
634          */
635         public Boolean isPrimary();
636
637         /**
638          * Returns a lookup (@link Uri) for the contact data item.
639          */
640         public Uri getDataUri();
641     }
642
643     /**
644      * Description of a specific {@link Data#_ID} item, with style information
645      * defined by a {@link DataKind}.
646      */
647     private static class DataAction implements Action {
648         private final Context mContext;
649         private final DataKind mKind;
650         private final String mMimeType;
651
652         private CharSequence mHeader;
653         private CharSequence mBody;
654         private Intent mIntent;
655
656         private boolean mAlternate;
657         private Uri mDataUri;
658         private boolean mIsPrimary;
659
660         /**
661          * Create an action from common {@link Data} elements.
662          */
663         public DataAction(Context context, String mimeType, DataKind kind,
664                 long dataId, Cursor cursor) {
665             mContext = context;
666             mKind = kind;
667             mMimeType = mimeType;
668
669             // Inflate strings from cursor
670             mAlternate = Constants.MIME_SMS_ADDRESS.equals(mimeType);
671             if (mAlternate && mKind.actionAltHeader != null) {
672                 mHeader = mKind.actionAltHeader.inflateUsing(context, cursor);
673             } else if (mKind.actionHeader != null) {
674                 mHeader = mKind.actionHeader.inflateUsing(context, cursor);
675             }
676
677             if (getAsInt(cursor, Data.IS_SUPER_PRIMARY) != 0) {
678                 mIsPrimary = true;
679             }
680
681             if (mKind.actionBody != null) {
682                 mBody = mKind.actionBody.inflateUsing(context, cursor);
683             }
684
685             mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
686
687             // Handle well-known MIME-types with special care
688             if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
689                 final String number = getAsString(cursor, Phone.NUMBER);
690                 if (!TextUtils.isEmpty(number)) {
691                     final Uri callUri = Uri.fromParts(Constants.SCHEME_TEL, number, null);
692                     mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
693                 }
694
695             } else if (Constants.MIME_SMS_ADDRESS.equals(mimeType)) {
696                 final String number = getAsString(cursor, Phone.NUMBER);
697                 if (!TextUtils.isEmpty(number)) {
698                     final Uri smsUri = Uri.fromParts(Constants.SCHEME_SMSTO, number, null);
699                     mIntent = new Intent(Intent.ACTION_SENDTO, smsUri);
700                 }
701
702             } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
703                 final String address = getAsString(cursor, Email.DATA);
704                 if (!TextUtils.isEmpty(address)) {
705                     final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null);
706                     mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
707                 }
708
709             } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
710                 final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(
711                         getAsString(cursor, Data.MIMETYPE));
712                 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
713                         getAsInt(cursor, Im.PROTOCOL);
714
715                 String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
716                 String data = getAsString(cursor, isEmail ? Email.DATA : Im.DATA);
717                 if (protocol != Im.PROTOCOL_CUSTOM) {
718                     // Try bringing in a well-known host for specific protocols
719                     host = ContactsUtils.lookupProviderNameFromId(protocol);
720                 }
721
722                 if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) {
723                     final String authority = host.toLowerCase();
724                     final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
725                             authority).appendPath(data).build();
726                     mIntent = new Intent(Intent.ACTION_SENDTO, imUri);
727                 }
728             }
729
730             if (mIntent == null) {
731                 // Otherwise fall back to default VIEW action
732                 mIntent = new Intent(Intent.ACTION_VIEW, mDataUri);
733             }
734
735             // Always launch as new task, since we're like a launcher
736             mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
737         }
738
739         /** {@inheritDoc} */
740         public CharSequence getHeader() {
741             return mHeader;
742         }
743
744         /** {@inheritDoc} */
745         public CharSequence getBody() {
746             return mBody;
747         }
748
749         /** {@inheritDoc} */
750         public String getMimeType() {
751             return mMimeType;
752         }
753
754         /** {@inheritDoc} */
755         public Uri getDataUri() {
756             return mDataUri;
757         }
758
759         /** {@inheritDoc} */
760         public Boolean isPrimary() {
761             return mIsPrimary;
762         }
763
764         /** {@inheritDoc} */
765         public Drawable getFallbackIcon() {
766             // Bail early if no valid resources
767             final String resPackageName = mKind.resPackageName;
768             if (resPackageName == null) return null;
769
770             final PackageManager pm = mContext.getPackageManager();
771             if (mAlternate && mKind.iconAltRes != -1) {
772                 return pm.getDrawable(resPackageName, mKind.iconAltRes, null);
773             } else if (mKind.iconRes != -1) {
774                 return pm.getDrawable(resPackageName, mKind.iconRes, null);
775             } else {
776                 return null;
777             }
778         }
779
780         /** {@inheritDoc} */
781         public Intent getIntent() {
782             return mIntent;
783         }
784     }
785
786     /**
787      * Specific action that launches the profile card.
788      */
789     private static class ProfileAction implements Action {
790         private final Context mContext;
791         private final Uri mLookupUri;
792
793         public ProfileAction(Context context, Uri lookupUri) {
794             mContext = context;
795             mLookupUri = lookupUri;
796         }
797
798         /** {@inheritDoc} */
799         public CharSequence getHeader() {
800             return null;
801         }
802
803         /** {@inheritDoc} */
804         public CharSequence getBody() {
805             return null;
806         }
807
808         /** {@inheritDoc} */
809         public String getMimeType() {
810             return Contacts.CONTENT_ITEM_TYPE;
811         }
812
813         /** {@inheritDoc} */
814         public Drawable getFallbackIcon() {
815             return mContext.getResources().getDrawable(R.drawable.ic_contacts_details);
816         }
817
818         /** {@inheritDoc} */
819         public Intent getIntent() {
820             final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
821             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
822             return intent;
823         }
824
825         /** {@inheritDoc} */
826         public Boolean isPrimary() {
827             return null;
828         }
829
830         /** {@inheritDoc} */
831         public Uri getDataUri() {
832             return null;
833         }
834
835     }
836
837     /**
838      * Internally hold a cache of scaled icons based on {@link PackageManager}
839      * queries, keyed internally on MIME-type.
840      */
841     private static class ResolveCache {
842         private Context mContext;
843         private PackageManager mPackageManager;
844
845         /**
846          * Cached entry holding the best {@link ResolveInfo} for a specific
847          * MIME-type, along with a {@link SoftReference} to its icon.
848          */
849         private static class Entry {
850             public ResolveInfo bestResolve;
851             public SoftReference<Drawable> icon;
852         }
853
854         private HashMap<String, Entry> mCache = new HashMap<String, Entry>();
855
856         public ResolveCache(Context context) {
857             mContext = context;
858             mPackageManager = context.getPackageManager();
859         }
860
861         /**
862          * Get the {@link Entry} best associated with the given {@link Action},
863          * or create and populate a new one if it doesn't exist.
864          */
865         protected Entry getEntry(Action action) {
866             final String mimeType = action.getMimeType();
867             Entry entry = mCache.get(mimeType);
868             if (entry != null) return entry;
869             entry = new Entry();
870
871             final Intent intent = action.getIntent();
872             if (intent != null) {
873                 final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
874                         PackageManager.MATCH_DEFAULT_ONLY);
875
876                 // Pick first match, otherwise best found
877                 ResolveInfo bestResolve = null;
878                 final int size = matches.size();
879                 if (size == 1) {
880                     bestResolve = matches.get(0);
881                 } else if (size > 1) {
882                     bestResolve = getBestResolve(matches);
883                 }
884
885                 if (bestResolve != null) {
886                     final Drawable icon = bestResolve.loadIcon(mPackageManager);
887
888                     entry.bestResolve = bestResolve;
889                     entry.icon = new SoftReference<Drawable>(icon);
890                 }
891             }
892
893             mCache.put(mimeType, entry);
894             return entry;
895         }
896
897         /**
898          * Best {@link ResolveInfo} when multiple found. Ties are broken by
899          * selecting first from the {QuickContactWindow#sPreferResolve} list of
900          * preferred packages, second by apps that live on the system partition,
901          * otherwise the app from the top of the list. This is
902          * <strong>only</strong> used for selecting a default icon for
903          * displaying in the track, and does not shortcut the system
904          * {@link Intent} disambiguation dialog.
905          */
906         protected ResolveInfo getBestResolve(List<ResolveInfo> matches) {
907             // Accept any package from prefer list, otherwise first system app
908             ResolveInfo firstSystem = null;
909             for (ResolveInfo info : matches) {
910                 final boolean isSystem = (info.activityInfo.applicationInfo.flags
911                         & ApplicationInfo.FLAG_SYSTEM) != 0;
912                 final boolean isPrefer = QuickContactWindow.sPreferResolve
913                         .contains(info.activityInfo.applicationInfo.packageName);
914
915                 if (isPrefer) return info;
916                 if (isSystem && firstSystem != null) firstSystem = info;
917             }
918
919             // Return first system found, otherwise first from list
920             return firstSystem != null ? firstSystem : matches.get(0);
921         }
922
923         /**
924          * Check {@link PackageManager} to see if any apps offer to handle the
925          * given {@link Action}.
926          */
927         public boolean hasResolve(Action action) {
928             return getEntry(action).bestResolve != null;
929         }
930
931         /**
932          * Find the best description for the given {@link Action}, usually used
933          * for accessibility purposes.
934          */
935         public CharSequence getDescription(Action action) {
936             final CharSequence actionHeader = action.getHeader();
937             final ResolveInfo info = getEntry(action).bestResolve;
938             if (!TextUtils.isEmpty(actionHeader)) {
939                 return actionHeader;
940             } else if (info != null) {
941                 return info.loadLabel(mPackageManager);
942             } else {
943                 return null;
944             }
945         }
946
947         /**
948          * Return the best icon for the given {@link Action}, which is usually
949          * based on the {@link ResolveInfo} found through a
950          * {@link PackageManager} query.
951          */
952         public Drawable getIcon(Action action) {
953             final SoftReference<Drawable> iconRef = getEntry(action).icon;
954             return (iconRef == null) ? null : iconRef.get();
955         }
956
957         public void clear() {
958             mCache.clear();
959         }
960     }
961
962     /**
963      * Provide a strongly-typed {@link LinkedList} that holds a list of
964      * {@link Action} objects.
965      */
966     private class ActionList extends LinkedList<Action> {
967     }
968
969     /**
970      * Provide a simple way of collecting one or more {@link Action} objects
971      * under a MIME-type key.
972      */
973     private class ActionMap extends HashMap<String, ActionList> {
974         private void collect(String mimeType, Action info) {
975             // Create list for this MIME-type when needed
976             ActionList collectList = get(mimeType);
977             if (collectList == null) {
978                 collectList = new ActionList();
979                 put(mimeType, collectList);
980             }
981             collectList.add(info);
982         }
983     }
984
985     /**
986      * Check if the given MIME-type appears in the list of excluded MIME-types
987      * that the most-recent caller requested.
988      */
989     private boolean isMimeExcluded(String mimeType) {
990         if (mExcludeMimes == null) return false;
991         for (String excludedMime : mExcludeMimes) {
992             if (TextUtils.equals(excludedMime, mimeType)) {
993                 return true;
994             }
995         }
996         return false;
997     }
998
999     /**
1000      * Internal storage for the latest social status, as built when walking
1001      * across a {@Link DataQuery} query. Will always keep record of at
1002      * least the first status it encounters, but will replace it with newer
1003      * statuses, as determined by timestamps.
1004      */
1005     private static class LatestStatus {
1006         private String mStatus = null;
1007         private long mTimestamp = -1;
1008
1009         private String mResPackage = null;
1010         private int mIconRes = -1;
1011         private int mLabelRes = -1;
1012
1013         private int getCursorInt(Cursor cursor, int columnIndex, int missingValue) {
1014             if (cursor.isNull(columnIndex)) return missingValue;
1015             return cursor.getInt(columnIndex);
1016         }
1017
1018         /**
1019          * Attempt updating this {@link LatestStatus} based on values at the
1020          * current row of the given {@link Cursor}. Assumes that query
1021          * projection was {@link DataQuery#PROJECTION}.
1022          */
1023         public void possibleUpdate(Cursor cursor) {
1024             final boolean hasStatus = !cursor.isNull(DataQuery.STATUS);
1025             final boolean hasTimestamp = !cursor.isNull(DataQuery.STATUS_TIMESTAMP);
1026
1027             // Bail early when not valid status, or when previous status was
1028             // found and we can't compare this one.
1029             if (!hasStatus) return;
1030             if (isValid() && !hasTimestamp) return;
1031
1032             if (hasTimestamp) {
1033                 // Compare timestamps and bail if older status
1034                 final long newTimestamp = cursor.getLong(DataQuery.STATUS_TIMESTAMP);
1035                 if (newTimestamp < mTimestamp) return;
1036
1037                 mTimestamp = newTimestamp;
1038             }
1039
1040             // Fill in remaining details from cursor
1041             mStatus = cursor.getString(DataQuery.STATUS);
1042             mResPackage = cursor.getString(DataQuery.STATUS_RES_PACKAGE);
1043             mIconRes = getCursorInt(cursor, DataQuery.STATUS_ICON, -1);
1044             mLabelRes = getCursorInt(cursor, DataQuery.STATUS_LABEL, -1);
1045         }
1046
1047         public boolean isValid() {
1048             return !TextUtils.isEmpty(mStatus);
1049         }
1050
1051         public CharSequence getStatus() {
1052             return mStatus;
1053         }
1054
1055         /**
1056          * Build any timestamp and label into a single string.
1057          */
1058         public CharSequence getTimestampLabel(Context context) {
1059             final PackageManager pm = context.getPackageManager();
1060
1061             final boolean validTimestamp = mTimestamp > 0;
1062             final boolean validLabel = mResPackage != null && mLabelRes != -1;
1063
1064             final CharSequence timeClause = validTimestamp ? DateUtils.getRelativeTimeSpanString(
1065                     mTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
1066                     DateUtils.FORMAT_ABBREV_RELATIVE) : null;
1067             final CharSequence labelClause = validLabel ? pm.getText(mResPackage, mLabelRes,
1068                     null) : null;
1069
1070             if (validTimestamp && validLabel) {
1071                 return context.getString(
1072                         com.android.internal.R.string.contact_status_update_attribution_with_date,
1073                         timeClause, labelClause);
1074             } else if (validLabel) {
1075                 return context.getString(
1076                         com.android.internal.R.string.contact_status_update_attribution,
1077                         labelClause);
1078             } else if (validTimestamp) {
1079                 return timeClause;
1080             } else {
1081                 return null;
1082             }
1083         }
1084
1085         public Drawable getIcon(Context context) {
1086             final PackageManager pm = context.getPackageManager();
1087             final boolean validIcon = mResPackage != null && mIconRes != -1;
1088             return validIcon ? pm.getDrawable(mResPackage, mIconRes, null) : null;
1089         }
1090     }
1091
1092     /**
1093      * Handle the result from the {@link #TOKEN_DATA} query.
1094      */
1095     private void handleData(Cursor cursor) {
1096         if (cursor == null) return;
1097
1098         if (!isMimeExcluded(Contacts.CONTENT_ITEM_TYPE)) {
1099             // Add the profile shortcut action
1100             final Action action = new ProfileAction(mContext, mLookupUri);
1101             mActions.collect(Contacts.CONTENT_ITEM_TYPE, action);
1102         }
1103
1104         final LatestStatus status = new LatestStatus();
1105         final Sources sources = Sources.getInstance(mContext);
1106         final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo);
1107
1108         Bitmap photoBitmap = null;
1109         while (cursor.moveToNext()) {
1110             final long dataId = cursor.getLong(DataQuery._ID);
1111             final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
1112             final String resPackage = cursor.getString(DataQuery.RES_PACKAGE);
1113             final String mimeType = cursor.getString(DataQuery.MIMETYPE);
1114
1115             // Handle any social status updates from this row
1116             status.possibleUpdate(cursor);
1117
1118             // Skip this data item if MIME-type excluded
1119             if (isMimeExcluded(mimeType)) continue;
1120
1121             // Handle photos included as data row
1122             if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
1123                 final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
1124                 final byte[] photoBlob = cursor.getBlob(colPhoto);
1125                 if (photoBlob != null) {
1126                     photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
1127                 }
1128                 continue;
1129             }
1130
1131             final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
1132                     ContactsSource.LEVEL_MIMETYPES);
1133
1134             if (kind != null) {
1135                 // Build an action for this data entry, find a mapping to a UI
1136                 // element, build its summary from the cursor, and collect it
1137                 // along with all others of this MIME-type.
1138                 final Action action = new DataAction(mContext, mimeType, kind, dataId, cursor);
1139                 considerAdd(action, mimeType);
1140             }
1141
1142             // If phone number, also insert as text message action
1143             if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && kind != null) {
1144                 final Action action = new DataAction(mContext, Constants.MIME_SMS_ADDRESS,
1145                         kind, dataId, cursor);
1146                 considerAdd(action, Constants.MIME_SMS_ADDRESS);
1147             }
1148
1149             // Handle Email rows with presence data as Im entry
1150             final boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE);
1151             if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
1152                 final DataKind imKind = sources.getKindOrFallback(accountType,
1153                         Im.CONTENT_ITEM_TYPE, mContext, ContactsSource.LEVEL_MIMETYPES);
1154                 if (imKind != null) {
1155                     final Action action = new DataAction(mContext, Im.CONTENT_ITEM_TYPE, imKind,
1156                             dataId, cursor);
1157                     considerAdd(action, Im.CONTENT_ITEM_TYPE);
1158                 }
1159             }
1160         }
1161
1162         if (cursor.moveToLast()) {
1163             // Read contact information from last data row
1164             final String name = cursor.getString(DataQuery.DISPLAY_NAME);
1165             final int presence = cursor.getInt(DataQuery.CONTACT_PRESENCE);
1166             final Drawable statusIcon = getPresenceIcon(presence);
1167
1168             setHeaderText(R.id.name, name);
1169             setHeaderImage(R.id.presence, statusIcon);
1170         }
1171
1172         if (photoView != null) {
1173             // Place photo when discovered in data, otherwise hide
1174             photoView.setVisibility(photoBitmap != null ? View.VISIBLE : View.GONE);
1175             photoView.setImageBitmap(photoBitmap);
1176         }
1177
1178         mHasValidSocial = status.isValid();
1179         if (mHasValidSocial && mMode != QuickContact.MODE_SMALL) {
1180             // Update status when valid was found
1181             setHeaderText(R.id.status, status.getStatus());
1182             setHeaderText(R.id.timestamp, status.getTimestampLabel(mContext));
1183
1184             final Drawable icon = status.getIcon(mContext);
1185             setHeaderImage(R.id.source, icon);
1186
1187             if (mMode == QuickContact.MODE_MEDIUM) {
1188                 // Hide medium divider when missing icon
1189                 final boolean validIcon = icon != null;
1190                 mHeader.findViewById(R.id.source_divider).setVisibility(
1191                         validIcon ? View.VISIBLE : View.GONE);
1192             }
1193         }
1194
1195         // Turn our list of actions into UI elements, starting with common types
1196         final Set<String> containedTypes = mActions.keySet();
1197         for (String mimeType : ORDERED_MIMETYPES) {
1198             if (containedTypes.contains(mimeType)) {
1199                 final int index = mTrack.getChildCount() - 1;
1200                 mTrack.addView(inflateAction(mimeType), index);
1201                 containedTypes.remove(mimeType);
1202             }
1203         }
1204
1205         // Then continue with remaining MIME-types in alphabetical order
1206         final String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
1207         Arrays.sort(remainingTypes);
1208         for (String mimeType : remainingTypes) {
1209             final int index = mTrack.getChildCount() - 1;
1210             mTrack.addView(inflateAction(mimeType), index);
1211         }
1212     }
1213
1214     /**
1215      * Consider adding the given {@link Action}, which will only happen if
1216      * {@link PackageManager} finds an application to handle
1217      * {@link Action#getIntent()}.
1218      */
1219     private void considerAdd(Action action, String mimeType) {
1220         if (mResolveCache.hasResolve(action)) {
1221             mActions.collect(mimeType, action);
1222         }
1223     }
1224
1225     /**
1226      * Obtain a new {@link CheckableImageView} for a new chiclet, either by
1227      * recycling one from {@link #mActionPool}, or by inflating a new one. When
1228      * finished, use {@link #releaseView(View)} to return back into the pool for
1229      * later recycling.
1230      */
1231     private synchronized View obtainView() {
1232         View view = mActionPool.poll();
1233         if (view == null || QuickContactActivity.FORCE_CREATE) {
1234             view = mInflater.inflate(R.layout.quickcontact_item, mTrack, false);
1235         }
1236         return view;
1237     }
1238
1239     /**
1240      * Return the given {@link CheckableImageView} into our internal pool for
1241      * possible recycling during another pass.
1242      */
1243     private synchronized void releaseView(View view) {
1244         mActionPool.offer(view);
1245         mActionRecycled++;
1246     }
1247
1248     /**
1249      * Inflate the in-track view for the action of the given MIME-type. Will use
1250      * the icon provided by the {@link DataKind}.
1251      */
1252     private View inflateAction(String mimeType) {
1253         final CheckableImageView view = (CheckableImageView)obtainView();
1254         boolean isActionSet = false;
1255
1256         // Add direct intent if single child, otherwise flag for multiple
1257         ActionList children = mActions.get(mimeType);
1258         Action firstInfo = children.get(0);
1259         if (children.size() == 1) {
1260             view.setTag(firstInfo);
1261         } else {
1262             for (Action action : children) {
1263                 if (action.isPrimary()) {
1264                     view.setTag(action);
1265                     isActionSet = true;
1266                     break;
1267                 }
1268             }
1269             if (!isActionSet) {
1270                 view.setTag(children);
1271             }
1272         }
1273
1274         // Set icon and listen for clicks
1275         final CharSequence descrip = mResolveCache.getDescription(firstInfo);
1276         final Drawable icon = mResolveCache.getIcon(firstInfo);
1277         view.setChecked(false);
1278         view.setContentDescription(descrip);
1279         view.setImageDrawable(icon);
1280         view.setOnClickListener(this);
1281         return view;
1282     }
1283
1284     /** {@inheritDoc} */
1285     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1286         // Pass list item clicks along so that Intents are handled uniformly
1287         onClick(view);
1288     }
1289
1290     /**
1291      * Flag indicating if {@link #mArrowDown} was visible during the last call
1292      * to {@link #setResolveVisible(boolean, CheckableImageView)}. Used to
1293      * decide during a later call if the arrow should be restored.
1294      */
1295     private boolean mWasDownArrow = false;
1296
1297     /**
1298      * Helper for showing and hiding {@link #mFooterDisambig}, which will
1299      * correctly manage {@link #mArrowDown} as needed.
1300      */
1301     private void setResolveVisible(boolean visible, CheckableImageView actionView) {
1302         // Show or hide the resolve list if needed
1303         boolean visibleNow = mFooterDisambig.getVisibility() == View.VISIBLE;
1304
1305         if (mLastAction != null) mLastAction.setChecked(false);
1306         if (actionView != null) actionView.setChecked(true);
1307         mLastAction = actionView;
1308
1309         // Bail early if already in desired state
1310         if (visible == visibleNow) return;
1311
1312         mFooter.setVisibility(visible ? View.GONE : View.VISIBLE);
1313         mFooterDisambig.setVisibility(visible ? View.VISIBLE : View.GONE);
1314
1315         if (visible) {
1316             // If showing list, then hide and save state of down arrow
1317             mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE);
1318             mArrowDown.setVisibility(View.INVISIBLE);
1319         } else {
1320             // If hiding list, restore any down arrow state
1321             mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE);
1322         }
1323     }
1324
1325     /** {@inheritDoc} */
1326     public void onClick(View view) {
1327         final boolean isActionView = (view instanceof CheckableImageView);
1328         final CheckableImageView actionView = isActionView ? (CheckableImageView)view : null;
1329         final Object tag = view.getTag();
1330         if (tag instanceof Action) {
1331             // Hide the resolution list, if present
1332             setResolveVisible(false, actionView);
1333             this.dismiss();
1334
1335             try {
1336                 // Incoming tag is concrete intent, so try launching
1337                 final Action action = (Action)tag;
1338                 mContext.startActivity(action.getIntent());
1339
1340                 if (mMakePrimary) {
1341                     ContentValues values = new ContentValues(1);
1342                     values.put(Data.IS_SUPER_PRIMARY, 1);
1343                     final Uri dataUri = action.getDataUri();
1344                     if (dataUri != null) {
1345                         mContext.getContentResolver().update(dataUri, values, null, null);
1346                     }
1347                 }
1348
1349             } catch (ActivityNotFoundException e) {
1350                 Toast.makeText(mContext, R.string.quickcontact_missing_app, Toast.LENGTH_SHORT)
1351                         .show();
1352             }
1353         } else if (tag instanceof ActionList) {
1354             // Incoming tag is a MIME-type, so show resolution list
1355             final ActionList children = (ActionList)tag;
1356
1357             // Show resolution list and set adapter
1358             setResolveVisible(true, actionView);
1359
1360             mResolveList.setOnItemClickListener(this);
1361             mResolveList.setAdapter(new BaseAdapter() {
1362                 public int getCount() {
1363                     return children.size();
1364                 }
1365
1366                 public Object getItem(int position) {
1367                     return children.get(position);
1368                 }
1369
1370                 public long getItemId(int position) {
1371                     return position;
1372                 }
1373
1374                 public View getView(int position, View convertView, ViewGroup parent) {
1375                     if (convertView == null) {
1376                         convertView = mInflater.inflate(
1377                                 R.layout.quickcontact_resolve_item, parent, false);
1378                     }
1379
1380                     // Set action title based on summary value
1381                     final Action action = (Action)getItem(position);
1382                     final Drawable icon = mResolveCache.getIcon(action);
1383
1384                     TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
1385                     TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
1386
1387                     text1.setText(action.getHeader());
1388                     text2.setText(action.getBody());
1389
1390                     convertView.setTag(action);
1391                     return convertView;
1392                 }
1393             });
1394
1395             // Make sure we resize to make room for ListView
1396             onWindowAttributesChanged(mWindow.getAttributes());
1397
1398         }
1399     }
1400
1401     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
1402         mMakePrimary = isChecked;
1403     }
1404
1405     private void onBackPressed() {
1406         // Back key will first dismiss any expanded resolve list, otherwise
1407         // it will close the entire dialog.
1408         if (mFooterDisambig.getVisibility() == View.VISIBLE) {
1409             setResolveVisible(false, null);
1410         } else {
1411             dismiss();
1412         }
1413     }
1414
1415     /** {@inheritDoc} */
1416     public boolean dispatchKeyEvent(KeyEvent event) {
1417         if (mWindow.superDispatchKeyEvent(event)) {
1418             return true;
1419         }
1420         return event.dispatch(this, mDecor != null
1421                 ? mDecor.getKeyDispatcherState() : null, this);
1422     }
1423
1424     /** {@inheritDoc} */
1425     public boolean onKeyDown(int keyCode, KeyEvent event) {
1426         if (keyCode == KeyEvent.KEYCODE_BACK) {
1427             event.startTracking();
1428             return true;
1429         }
1430
1431         return false;
1432     }
1433
1434     /** {@inheritDoc} */
1435     public boolean onKeyUp(int keyCode, KeyEvent event) {
1436         if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
1437                 && !event.isCanceled()) {
1438             onBackPressed();
1439             return true;
1440         }
1441
1442         return false;
1443     }
1444
1445     /** {@inheritDoc} */
1446     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
1447         return false;
1448     }
1449
1450     /** {@inheritDoc} */
1451     public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
1452         return false;
1453     }
1454
1455     /** {@inheritDoc} */
1456     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
1457         // TODO: make this window accessible
1458         return false;
1459     }
1460
1461     /**
1462      * Detect if the given {@link MotionEvent} is outside the boundaries of this
1463      * window, which usually means we should dismiss.
1464      */
1465     protected void detectEventOutside(MotionEvent event) {
1466         if (event.getAction() == MotionEvent.ACTION_DOWN) {
1467             // Only try detecting outside events on down-press
1468             mDecor.getHitRect(mRect);
1469             mRect.top = mRect.top + mDecor.getPaddingTop();
1470             mRect.bottom = mRect.bottom - mDecor.getPaddingBottom();
1471             final int x = (int)event.getX();
1472             final int y = (int)event.getY();
1473             if (!mRect.contains(x, y)) {
1474                 event.setAction(MotionEvent.ACTION_OUTSIDE);
1475             }
1476         }
1477     }
1478
1479     /** {@inheritDoc} */
1480     public boolean dispatchTouchEvent(MotionEvent event) {
1481         detectEventOutside(event);
1482         if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
1483             dismiss();
1484             return true;
1485         } else {
1486             return mWindow.superDispatchTouchEvent(event);
1487         }
1488     }
1489
1490     /** {@inheritDoc} */
1491     public boolean dispatchTrackballEvent(MotionEvent event) {
1492         return mWindow.superDispatchTrackballEvent(event);
1493     }
1494
1495     /** {@inheritDoc} */
1496     public void onContentChanged() {
1497     }
1498
1499     /** {@inheritDoc} */
1500     public boolean onCreatePanelMenu(int featureId, Menu menu) {
1501         return false;
1502     }
1503
1504     /** {@inheritDoc} */
1505     public View onCreatePanelView(int featureId) {
1506         return null;
1507     }
1508
1509     /** {@inheritDoc} */
1510     public boolean onMenuItemSelected(int featureId, MenuItem item) {
1511         return false;
1512     }
1513
1514     /** {@inheritDoc} */
1515     public boolean onMenuOpened(int featureId, Menu menu) {
1516         return false;
1517     }
1518
1519     /** {@inheritDoc} */
1520     public void onPanelClosed(int featureId, Menu menu) {
1521     }
1522
1523     /** {@inheritDoc} */
1524     public boolean onPreparePanel(int featureId, View view, Menu menu) {
1525         return false;
1526     }
1527
1528     /** {@inheritDoc} */
1529     public boolean onSearchRequested() {
1530         return false;
1531     }
1532
1533     /** {@inheritDoc} */
1534     public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
1535         if (mDecor != null) {
1536             mWindowManager.updateViewLayout(mDecor, attrs);
1537         }
1538     }
1539
1540     /** {@inheritDoc} */
1541     public void onWindowFocusChanged(boolean hasFocus) {
1542     }
1543
1544     /** {@inheritDoc} */
1545     public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
1546         // No actions
1547     }
1548
1549     /** {@inheritDoc} */
1550     public void onAttachedToWindow() {
1551         // No actions
1552     }
1553
1554     /** {@inheritDoc} */
1555     public void onDetachedFromWindow() {
1556         // No actions
1557     }
1558
1559     private interface DataQuery {
1560         final String[] PROJECTION = new String[] {
1561                 Data._ID,
1562
1563                 RawContacts.ACCOUNT_TYPE,
1564                 Contacts.STARRED,
1565                 Contacts.DISPLAY_NAME,
1566                 Contacts.CONTACT_PRESENCE,
1567
1568                 Data.STATUS,
1569                 Data.STATUS_RES_PACKAGE,
1570                 Data.STATUS_ICON,
1571                 Data.STATUS_LABEL,
1572                 Data.STATUS_TIMESTAMP,
1573                 Data.PRESENCE,
1574
1575                 Data.RES_PACKAGE,
1576                 Data.MIMETYPE,
1577                 Data.IS_PRIMARY,
1578                 Data.IS_SUPER_PRIMARY,
1579                 Data.RAW_CONTACT_ID,
1580
1581                 Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
1582                 Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11,
1583                 Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
1584         };
1585
1586         final int _ID = 0;
1587
1588         final int ACCOUNT_TYPE = 1;
1589         final int STARRED = 2;
1590         final int DISPLAY_NAME = 3;
1591         final int CONTACT_PRESENCE = 4;
1592
1593         final int STATUS = 5;
1594         final int STATUS_RES_PACKAGE = 6;
1595         final int STATUS_ICON = 7;
1596         final int STATUS_LABEL = 8;
1597         final int STATUS_TIMESTAMP = 9;
1598         final int PRESENCE = 10;
1599
1600         final int RES_PACKAGE = 11;
1601         final int MIMETYPE = 12;
1602         final int IS_PRIMARY = 13;
1603         final int IS_SUPER_PRIMARY = 14;
1604     }
1605 }