OSDN Git Service

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