OSDN Git Service

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