OSDN Git Service

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