OSDN Git Service

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