OSDN Git Service

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