OSDN Git Service

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