2 * Copyright (C) 2009 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.contacts.ui;
19 import com.android.contacts.Collapser;
20 import com.android.contacts.ContactPresenceIconUtil;
21 import com.android.contacts.ContactsUtils;
22 import com.android.contacts.R;
23 import com.android.contacts.model.ContactsSource;
24 import com.android.contacts.model.Sources;
25 import com.android.contacts.model.ContactsSource.DataKind;
26 import com.android.contacts.ui.widget.CheckableImageView;
27 import com.android.contacts.util.Constants;
28 import com.android.contacts.util.DataStatus;
29 import com.android.contacts.util.NotifyingAsyncQueryHandler;
30 import com.android.internal.policy.PolicyManager;
31 import com.google.android.collect.Sets;
33 import android.content.ActivityNotFoundException;
34 import android.content.ContentUris;
35 import android.content.ContentValues;
36 import android.content.Context;
37 import android.content.EntityIterator;
38 import android.content.Intent;
39 import android.content.IntentFilter;
40 import android.content.pm.ApplicationInfo;
41 import android.content.pm.PackageManager;
42 import android.content.pm.ResolveInfo;
43 import android.content.res.Resources;
44 import android.database.Cursor;
45 import android.graphics.Bitmap;
46 import android.graphics.BitmapFactory;
47 import android.graphics.Rect;
48 import android.graphics.drawable.Drawable;
49 import android.net.Uri;
50 import android.provider.ContactsContract.Contacts;
51 import android.provider.ContactsContract.Data;
52 import android.provider.ContactsContract.QuickContact;
53 import android.provider.ContactsContract.RawContacts;
54 import android.provider.ContactsContract.StatusUpdates;
55 import android.provider.ContactsContract.CommonDataKinds.Email;
56 import android.provider.ContactsContract.CommonDataKinds.Im;
57 import android.provider.ContactsContract.CommonDataKinds.Phone;
58 import android.provider.ContactsContract.CommonDataKinds.Photo;
59 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
60 import android.provider.ContactsContract.CommonDataKinds.Website;
61 import android.text.TextUtils;
62 import android.util.Log;
63 import android.view.ContextThemeWrapper;
64 import android.view.Gravity;
65 import android.view.KeyEvent;
66 import android.view.LayoutInflater;
67 import android.view.Menu;
68 import android.view.MenuItem;
69 import android.view.MotionEvent;
70 import android.view.View;
71 import android.view.ViewGroup;
72 import android.view.ViewStub;
73 import android.view.Window;
74 import android.view.WindowManager;
75 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
76 import android.view.accessibility.AccessibilityEvent;
77 import android.view.animation.Animation;
78 import android.view.animation.AnimationUtils;
79 import android.view.animation.Interpolator;
80 import android.widget.AbsListView;
81 import android.widget.AdapterView;
82 import android.widget.BaseAdapter;
83 import android.widget.CheckBox;
84 import android.widget.CompoundButton;
85 import android.widget.HorizontalScrollView;
86 import android.widget.ImageView;
87 import android.widget.ListView;
88 import android.widget.TextView;
89 import android.widget.Toast;
91 import java.lang.ref.SoftReference;
92 import java.util.ArrayList;
93 import java.util.Arrays;
94 import java.util.HashMap;
95 import java.util.HashSet;
96 import java.util.LinkedList;
97 import java.util.List;
101 * Window that shows QuickContact dialog for a specific {@link Contacts#_ID}.
103 public class QuickContactWindow implements Window.Callback,
104 NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener,
105 AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback,
106 OnGlobalLayoutListener {
107 private static final String TAG = "QuickContactWindow";
110 * Interface used to allow the person showing a {@link QuickContactWindow} to
111 * know when the window has been dismissed.
113 public interface OnDismissListener {
114 public void onDismiss(QuickContactWindow dialog);
117 private final Context mContext;
118 private final LayoutInflater mInflater;
119 private final WindowManager mWindowManager;
120 private Window mWindow;
122 private final Rect mRect = new Rect();
124 private boolean mDismissed = false;
125 private boolean mQuerying = false;
126 private boolean mShowing = false;
128 private NotifyingAsyncQueryHandler mHandler;
129 private OnDismissListener mDismissListener;
130 private ResolveCache mResolveCache;
132 private Uri mLookupUri;
133 private Rect mAnchor;
135 private int mShadowHoriz;
136 private int mShadowVert;
137 private int mShadowTouch;
139 private int mScreenWidth;
140 private int mScreenHeight;
141 private int mRequestedY;
143 private boolean mHasValidSocial = false;
144 private boolean mHasData = false;
145 private boolean mMakePrimary = false;
147 private ImageView mArrowUp;
148 private ImageView mArrowDown;
151 private View mHeader;
152 private HorizontalScrollView mTrackScroll;
153 private ViewGroup mTrack;
154 private Animation mTrackAnim;
156 private View mFooter;
157 private View mFooterDisambig;
158 private ListView mResolveList;
159 private CheckableImageView mLastAction;
160 private CheckBox mSetPrimaryCheckBox;
162 private int mWindowRecycled = 0;
163 private int mActionRecycled = 0;
166 * Set of {@link Action} that are associated with the aggregate currently
167 * displayed by this dialog, represented as a map from {@link String}
168 * MIME-type to {@link ActionList}.
170 private ActionMap mActions = new ActionMap();
173 * Pool of unused {@link CheckableImageView} that have previously been
174 * inflated, and are ready to be recycled through {@link #obtainView()}.
176 private LinkedList<View> mActionPool = new LinkedList<View>();
178 private String[] mExcludeMimes;
181 * {@link #PRECEDING_MIMETYPES} and {@link #FOLLOWING_MIMETYPES} are used to sort MIME-types.
183 * <p>The MIME-types in {@link #PRECEDING_MIMETYPES} appear in the front of the dialog,
184 * in the order in the array.
186 * <p>The ones in {@link #FOLLOWING_MIMETYPES} appear in the end of the dialog, in alphabetical
189 * <p>The rest go between them, in the order in the array.
191 private static final String[] PRECEDING_MIMETYPES = new String[] {
192 Phone.CONTENT_ITEM_TYPE,
193 Contacts.CONTENT_ITEM_TYPE,
194 Constants.MIME_SMS_ADDRESS,
195 Email.CONTENT_ITEM_TYPE,
199 * See {@link #PRECEDING_MIMETYPES}.
201 private static final String[] FOLLOWING_MIMETYPES = new String[] {
202 StructuredPostal.CONTENT_ITEM_TYPE,
203 Website.CONTENT_ITEM_TYPE,
207 * Specific list {@link ApplicationInfo#packageName} of apps that are
208 * prefered <strong>only</strong> for the purposes of default icons when
209 * multiple {@link ResolveInfo} are found to match. This only happens when
210 * the user has not selected a default app yet, and they will still be
211 * presented with the system disambiguation dialog.
213 private static final HashSet<String> sPreferResolve = Sets.newHashSet(
215 "com.android.calendar",
216 "com.android.contacts",
219 "com.android.browser");
221 private static final int TOKEN_DATA = 1;
223 static final boolean LOGD = false;
225 static final boolean TRACE_LAUNCH = false;
226 static final String TRACE_TAG = "quickcontact";
229 * Prepare a dialog to show in the given {@link Context}.
231 public QuickContactWindow(Context context) {
232 mContext = new ContextThemeWrapper(context, R.style.QuickContact);
233 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
234 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
236 mWindow = PolicyManager.makeNewWindow(mContext);
237 mWindow.setCallback(this);
238 mWindow.setWindowManager(mWindowManager, null, null);
240 mWindow.setContentView(R.layout.quickcontact);
242 mArrowUp = (ImageView)mWindow.findViewById(R.id.arrow_up);
243 mArrowDown = (ImageView)mWindow.findViewById(R.id.arrow_down);
245 mResolveCache = new ResolveCache(mContext);
247 final Resources res = mContext.getResources();
248 mShadowHoriz = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_horiz);
249 mShadowVert = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_vert);
250 mShadowTouch = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_touch);
252 mScreenWidth = mWindowManager.getDefaultDisplay().getWidth();
253 mScreenHeight = mWindowManager.getDefaultDisplay().getHeight();
255 mTrack = (ViewGroup)mWindow.findViewById(R.id.quickcontact);
256 mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
258 mFooter = mWindow.findViewById(R.id.footer);
259 mFooterDisambig = mWindow.findViewById(R.id.footer_disambig);
260 mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
261 mSetPrimaryCheckBox = (CheckBox)mWindow.findViewById(android.R.id.checkbox);
263 mSetPrimaryCheckBox.setOnCheckedChangeListener(this);
265 // Prepare track entrance animation
266 mTrackAnim = AnimationUtils.loadAnimation(mContext, R.anim.quickcontact);
267 mTrackAnim.setInterpolator(new Interpolator() {
268 public float getInterpolation(float t) {
269 // Pushes past the target area, then snaps back into place.
270 // Equation for graphing: 1.2-((x*1.6)-1.1)^2
271 final float inner = (t * 1.55f) - 1.1f;
272 return 1.2f - inner * inner;
276 mHandler = new NotifyingAsyncQueryHandler(mContext, this);
280 * Prepare a dialog to show in the given {@link Context}, and notify the
281 * given {@link OnDismissListener} each time this dialog is dismissed.
283 public QuickContactWindow(Context context, OnDismissListener dismissListener) {
285 mDismissListener = dismissListener;
288 private View getHeaderView(int mode) {
291 case QuickContact.MODE_SMALL:
292 header = mWindow.findViewById(R.id.header_small);
294 case QuickContact.MODE_MEDIUM:
295 header = mWindow.findViewById(R.id.header_medium);
297 case QuickContact.MODE_LARGE:
298 header = mWindow.findViewById(R.id.header_large);
302 if (header instanceof ViewStub) {
303 // Inflate actual header if we picked a stub
304 final ViewStub stub = (ViewStub)header;
305 header = stub.inflate();
306 } else if (header != null) {
307 header.setVisibility(View.VISIBLE);
314 * Start showing a dialog for the given {@link Contacts#_ID} pointing
315 * towards the given location.
317 public synchronized void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
318 if (mQuerying || mShowing) {
319 Log.w(TAG, "dismissing before showing");
323 if (TRACE_LAUNCH && !android.os.Debug.isMethodTracingActive()) {
324 android.os.Debug.startMethodTracing(TRACE_TAG);
327 // Validate incoming parameters
328 final boolean validMode = (mode == QuickContact.MODE_SMALL
329 || mode == QuickContact.MODE_MEDIUM || mode == QuickContact.MODE_LARGE);
331 throw new IllegalArgumentException("Invalid mode, expecting MODE_LARGE, "
332 + "MODE_MEDIUM, or MODE_SMALL");
335 if (anchor == null) {
336 throw new IllegalArgumentException("Missing anchor rectangle");
339 // Prepare header view for requested mode
340 mLookupUri = lookupUri;
341 mAnchor = new Rect(anchor);
343 mExcludeMimes = excludeMimes;
345 mHeader = getHeaderView(mode);
347 setHeaderText(R.id.name, R.string.quickcontact_missing_name);
349 setHeaderText(R.id.status, null);
350 setHeaderText(R.id.timestamp, null);
352 setHeaderImage(R.id.presence, null);
356 mHasValidSocial = false;
360 // Start background query for data, but only select photo rows when they
361 // directly match the super-primary PHOTO_ID.
362 final Uri dataUri = getDataUri(lookupUri);
363 mHandler.cancelOperation(TOKEN_DATA);
365 // Only request photo data when required by mode
366 if (mMode == QuickContact.MODE_LARGE) {
367 // Select photos, but only super-primary
368 mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
369 + "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
370 + ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
372 // Exclude all photos from cursor
373 mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
374 + "!=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
379 * Build a {@link Uri} into the {@link Data} table for the requested
380 * {@link Contacts#CONTENT_LOOKUP_URI} style {@link Uri}.
382 private Uri getDataUri(Uri lookupUri) {
383 // TODO: Formalize method of extracting LOOKUP_KEY
384 final List<String> path = lookupUri.getPathSegments();
385 final boolean validLookup = path.size() >= 3 && "lookup".equals(path.get(1));
387 // We only accept valid lookup-style Uris
388 throw new IllegalArgumentException("Expecting lookup-style Uri");
389 } else if (path.size() == 3) {
390 // No direct _ID provided, so force a lookup
391 lookupUri = Contacts.lookupContact(mContext.getContentResolver(), lookupUri);
394 final long contactId = ContentUris.parseId(lookupUri);
395 return Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
396 Contacts.Data.CONTENT_DIRECTORY);
400 * Show the correct call-out arrow based on a {@link R.id} reference.
402 private void showArrow(int whichArrow, int requestedX) {
403 final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
404 final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
406 final int arrowWidth = mArrowUp.getMeasuredWidth();
408 showArrow.setVisibility(View.VISIBLE);
409 ViewGroup.MarginLayoutParams param = (ViewGroup.MarginLayoutParams)showArrow.getLayoutParams();
410 param.leftMargin = requestedX - arrowWidth / 2;
412 hideArrow.setVisibility(View.INVISIBLE);
416 * Actual internal method to show this dialog. Called only by
417 * {@link #considerShowing()} when all data requirements have been met.
419 private void showInternal() {
420 mDecor = mWindow.getDecorView();
421 mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this);
422 WindowManager.LayoutParams l = mWindow.getAttributes();
424 l.width = mScreenWidth + mShadowHoriz + mShadowHoriz;
425 l.height = WindowManager.LayoutParams.WRAP_CONTENT;
427 // Force layout measuring pass so we have baseline numbers
428 mDecor.measure(l.width, l.height);
429 final int blockHeight = mDecor.getMeasuredHeight();
431 l.gravity = Gravity.TOP | Gravity.LEFT;
434 if (mAnchor.top > blockHeight) {
435 // Show downwards callout when enough room, aligning bottom block
436 // edge with top of anchor area, and adjusting to inset arrow.
437 showArrow(R.id.arrow_down, mAnchor.centerX());
438 l.y = mAnchor.top - blockHeight + mShadowVert;
439 l.windowAnimations = R.style.QuickContactAboveAnimation;
442 // Otherwise show upwards callout, aligning block top with bottom of
443 // anchor area, and adjusting to inset arrow.
444 showArrow(R.id.arrow_up, mAnchor.centerX());
445 l.y = mAnchor.bottom - mShadowVert;
446 l.windowAnimations = R.style.QuickContactBelowAnimation;
450 l.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
451 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
454 mWindowManager.addView(mDecor, l);
459 mTrack.startAnimation(mTrackAnim);
462 android.os.Debug.stopMethodTracing();
463 Log.d(TAG, "Window recycled " + mWindowRecycled + " times, chiclets "
464 + mActionRecycled + " times");
469 public void onGlobalLayout() {
474 * Adjust vertical {@link WindowManager.LayoutParams} to fit window as best
475 * as possible, shifting up to display content as needed.
477 private void layoutInScreen() {
478 if (!mShowing) return;
480 final WindowManager.LayoutParams l = mWindow.getAttributes();
481 final int originalY = l.y;
483 final int blockHeight = mDecor.getHeight();
486 if (mRequestedY + blockHeight > mScreenHeight) {
487 // Shift up from bottom when overflowing
488 l.y = mScreenHeight - blockHeight;
491 if (originalY != l.y) {
492 // Only update when value is changed
493 mWindow.setAttributes(l);
498 * Dismiss this dialog if showing.
500 public synchronized void dismiss() {
501 // Notify any listeners that we've been dismissed
502 if (mDismissListener != null) {
503 mDismissListener.onDismiss(this);
509 private void dismissInternal() {
510 // Remove any attached window decor for recycling
511 boolean hadDecor = mDecor != null;
513 mWindowManager.removeView(mDecor);
515 mDecor.getViewTreeObserver().removeGlobalOnLayoutListener(this);
517 mWindow.closeAllPanels();
522 // Cancel any pending queries
523 mHandler.cancelOperation(TOKEN_DATA);
526 // Completely hide header and reset track
527 mHeader.setVisibility(View.GONE);
532 * Reset track to initial state, recycling any chiclets.
534 private void resetTrack() {
535 // Release reference to last chiclet
538 // Clear track actions and scroll to hard left
539 mResolveCache.clear();
542 // Recycle any chiclets in use
543 while (mTrack.getChildCount() > 2) {
544 this.releaseView(mTrack.getChildAt(1));
545 mTrack.removeViewAt(1);
548 mTrackScroll.fullScroll(View.FOCUS_LEFT);
549 mWasDownArrow = false;
551 // Clear any primary requests
552 mMakePrimary = false;
553 mSetPrimaryCheckBox.setChecked(false);
555 setResolveVisible(false, null);
559 * Consider showing this window, which will only call through to
560 * {@link #showInternal()} when all data items are present.
562 private void considerShowing() {
563 if (mHasData && !mShowing && !mDismissed) {
564 if (mMode == QuickContact.MODE_MEDIUM && !mHasValidSocial) {
565 // Missing valid social, swap medium for small header
566 mHeader.setVisibility(View.GONE);
567 mHeader = getHeaderView(QuickContact.MODE_SMALL);
570 // All queries have returned, pull curtain
576 public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
577 // Bail early when query is stale
578 if (cookie != mLookupUri) return;
580 if (cursor == null) {
581 // Problem while running query, so bail without showing
582 Log.w(TAG, "Missing cursor for token=" + token);
590 if (!cursor.isClosed()) {
597 /** Assign this string to the view, if found in {@link #mHeader}. */
598 private void setHeaderText(int id, int resId) {
599 setHeaderText(id, mContext.getResources().getText(resId));
602 /** Assign this string to the view, if found in {@link #mHeader}. */
603 private void setHeaderText(int id, CharSequence value) {
604 final View view = mHeader.findViewById(id);
605 if (view instanceof TextView) {
606 ((TextView)view).setText(value);
607 view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE);
611 /** Assign this image to the view, if found in {@link #mHeader}. */
612 private void setHeaderImage(int id, Drawable drawable) {
613 final View view = mHeader.findViewById(id);
614 if (view instanceof ImageView) {
615 ((ImageView)view).setImageDrawable(drawable);
616 view.setVisibility(drawable == null ? View.GONE : View.VISIBLE);
621 * Find the QuickContact-specific presence icon for showing in chiclets.
623 private Drawable getTrackPresenceIcon(int status) {
626 case StatusUpdates.AVAILABLE:
627 resId = R.drawable.quickcontact_slider_presence_active;
629 case StatusUpdates.IDLE:
630 case StatusUpdates.AWAY:
631 resId = R.drawable.quickcontact_slider_presence_away;
633 case StatusUpdates.DO_NOT_DISTURB:
634 resId = R.drawable.quickcontact_slider_presence_busy;
636 case StatusUpdates.INVISIBLE:
637 resId = R.drawable.quickcontact_slider_presence_inactive;
639 case StatusUpdates.OFFLINE:
641 resId = R.drawable.quickcontact_slider_presence_inactive;
643 return mContext.getResources().getDrawable(resId);
646 /** Read {@link String} from the given {@link Cursor}. */
647 private static String getAsString(Cursor cursor, String columnName) {
648 final int index = cursor.getColumnIndex(columnName);
649 return cursor.getString(index);
652 /** Read {@link Integer} from the given {@link Cursor}. */
653 private static int getAsInt(Cursor cursor, String columnName) {
654 final int index = cursor.getColumnIndex(columnName);
655 return cursor.getInt(index);
659 * Abstract definition of an action that could be performed, along with
660 * string description and icon.
662 private interface Action extends Collapser.Collapsible<Action> {
663 public CharSequence getHeader();
664 public CharSequence getBody();
666 public String getMimeType();
667 public Drawable getFallbackIcon();
670 * Build an {@link Intent} that will perform this action.
672 public Intent getIntent();
675 * Checks if the contact data for this action is primary.
677 public Boolean isPrimary();
680 * Returns a lookup (@link Uri) for the contact data item.
682 public Uri getDataUri();
686 * Description of a specific {@link Data#_ID} item, with style information
687 * defined by a {@link DataKind}.
689 private static class DataAction implements Action {
690 private final Context mContext;
691 private final DataKind mKind;
692 private final String mMimeType;
694 private CharSequence mHeader;
695 private CharSequence mBody;
696 private Intent mIntent;
698 private boolean mAlternate;
699 private Uri mDataUri;
700 private boolean mIsPrimary;
703 * Create an action from common {@link Data} elements.
705 public DataAction(Context context, String mimeType, DataKind kind,
706 long dataId, Cursor cursor) {
709 mMimeType = mimeType;
711 // Inflate strings from cursor
712 mAlternate = Constants.MIME_SMS_ADDRESS.equals(mimeType);
713 if (mAlternate && mKind.actionAltHeader != null) {
714 mHeader = mKind.actionAltHeader.inflateUsing(context, cursor);
715 } else if (mKind.actionHeader != null) {
716 mHeader = mKind.actionHeader.inflateUsing(context, cursor);
719 if (getAsInt(cursor, Data.IS_SUPER_PRIMARY) != 0) {
723 if (mKind.actionBody != null) {
724 mBody = mKind.actionBody.inflateUsing(context, cursor);
727 mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
729 // Handle well-known MIME-types with special care
730 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
731 final String number = getAsString(cursor, Phone.NUMBER);
732 if (!TextUtils.isEmpty(number)) {
733 final Uri callUri = Uri.fromParts(Constants.SCHEME_TEL, number, null);
734 mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
737 } else if (Constants.MIME_SMS_ADDRESS.equals(mimeType)) {
738 final String number = getAsString(cursor, Phone.NUMBER);
739 if (!TextUtils.isEmpty(number)) {
740 final Uri smsUri = Uri.fromParts(Constants.SCHEME_SMSTO, number, null);
741 mIntent = new Intent(Intent.ACTION_SENDTO, smsUri);
744 } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
745 final String address = getAsString(cursor, Email.DATA);
746 if (!TextUtils.isEmpty(address)) {
747 final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null);
748 mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
751 } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
752 final String url = getAsString(cursor, Website.URL);
753 if (!TextUtils.isEmpty(url)) {
754 mIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
757 } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
758 final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(
759 getAsString(cursor, Data.MIMETYPE));
760 if (isEmail || isProtocolValid(cursor)) {
761 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
762 getAsInt(cursor, Im.PROTOCOL);
765 // Use Google Talk string when using Email, and clear data
766 // Uri so we don't try saving Email as primary.
767 mHeader = context.getText(R.string.chat_gtalk);
771 String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
772 String data = getAsString(cursor, isEmail ? Email.DATA : Im.DATA);
773 if (protocol != Im.PROTOCOL_CUSTOM) {
774 // Try bringing in a well-known host for specific protocols
775 host = ContactsUtils.lookupProviderNameFromId(protocol);
778 if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) {
779 final String authority = host.toLowerCase();
780 final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
781 authority).appendPath(data).build();
782 mIntent = new Intent(Intent.ACTION_SENDTO, imUri);
787 if (mIntent == null) {
788 // Otherwise fall back to default VIEW action
789 mIntent = new Intent(Intent.ACTION_VIEW, mDataUri);
792 // Always launch as new task, since we're like a launcher
793 mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
796 private boolean isProtocolValid(Cursor cursor) {
797 final int columnIndex = cursor.getColumnIndex(Im.PROTOCOL);
798 if (cursor.isNull(columnIndex)) {
802 Integer.valueOf(cursor.getString(columnIndex));
803 } catch (NumberFormatException e) {
810 public CharSequence getHeader() {
815 public CharSequence getBody() {
820 public String getMimeType() {
825 public Uri getDataUri() {
830 public Boolean isPrimary() {
835 public Drawable getFallbackIcon() {
836 // Bail early if no valid resources
837 final String resPackageName = mKind.resPackageName;
838 if (resPackageName == null) return null;
840 final PackageManager pm = mContext.getPackageManager();
841 if (mAlternate && mKind.iconAltRes != -1) {
842 return pm.getDrawable(resPackageName, mKind.iconAltRes, null);
843 } else if (mKind.iconRes != -1) {
844 return pm.getDrawable(resPackageName, mKind.iconRes, null);
851 public Intent getIntent() {
856 public boolean collapseWith(Action other) {
857 if (!shouldCollapseWith(other)) {
864 public boolean shouldCollapseWith(Action t) {
868 if (!(t instanceof DataAction)) {
869 Log.e(TAG, "t must be DataAction");
872 DataAction other = (DataAction)t;
873 if (!ContactsUtils.areObjectsEqual(mKind, other.mKind)) {
876 if (!ContactsUtils.areDataEqual(mContext, mMimeType, mBody, other.mMimeType,
880 if (!TextUtils.equals(mMimeType, other.mMimeType)
881 || !ContactsUtils.areIntentActionEqual(mIntent, other.mIntent)
890 * Specific action that launches the profile card.
892 private static class ProfileAction implements Action {
893 private final Context mContext;
894 private final Uri mLookupUri;
896 public ProfileAction(Context context, Uri lookupUri) {
898 mLookupUri = lookupUri;
902 public CharSequence getHeader() {
907 public CharSequence getBody() {
912 public String getMimeType() {
913 return Contacts.CONTENT_ITEM_TYPE;
917 public Drawable getFallbackIcon() {
918 return mContext.getResources().getDrawable(R.drawable.ic_contacts_details);
922 public Intent getIntent() {
923 final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
924 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
929 public Boolean isPrimary() {
934 public Uri getDataUri() {
939 public boolean collapseWith(Action t) {
940 return false; // Never dup.
944 public boolean shouldCollapseWith(Action t) {
945 return false; // Never dup.
950 * Internally hold a cache of scaled icons based on {@link PackageManager}
951 * queries, keyed internally on MIME-type.
953 private static class ResolveCache {
954 private PackageManager mPackageManager;
957 * Cached entry holding the best {@link ResolveInfo} for a specific
958 * MIME-type, along with a {@link SoftReference} to its icon.
960 private static class Entry {
961 public ResolveInfo bestResolve;
962 public SoftReference<Drawable> icon;
965 private HashMap<String, Entry> mCache = new HashMap<String, Entry>();
967 public ResolveCache(Context context) {
968 mPackageManager = context.getPackageManager();
972 * Get the {@link Entry} best associated with the given {@link Action},
973 * or create and populate a new one if it doesn't exist.
975 protected Entry getEntry(Action action) {
976 final String mimeType = action.getMimeType();
977 Entry entry = mCache.get(mimeType);
978 if (entry != null) return entry;
981 final Intent intent = action.getIntent();
982 if (intent != null) {
983 final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
984 PackageManager.MATCH_DEFAULT_ONLY);
986 // Pick first match, otherwise best found
987 ResolveInfo bestResolve = null;
988 final int size = matches.size();
990 bestResolve = matches.get(0);
991 } else if (size > 1) {
992 bestResolve = getBestResolve(intent, matches);
995 if (bestResolve != null) {
996 final Drawable icon = bestResolve.loadIcon(mPackageManager);
998 entry.bestResolve = bestResolve;
999 entry.icon = new SoftReference<Drawable>(icon);
1003 mCache.put(mimeType, entry);
1008 * Best {@link ResolveInfo} when multiple found. Ties are broken by
1009 * selecting first from the {QuickContactWindow#sPreferResolve} list of
1010 * preferred packages, second by apps that live on the system partition,
1011 * otherwise the app from the top of the list. This is
1012 * <strong>only</strong> used for selecting a default icon for
1013 * displaying in the track, and does not shortcut the system
1014 * {@link Intent} disambiguation dialog.
1016 protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) {
1017 // Try finding preferred activity, otherwise detect disambig
1018 final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent,
1019 PackageManager.MATCH_DEFAULT_ONLY);
1020 final boolean foundDisambig = (foundResolve.match &
1021 IntentFilter.MATCH_CATEGORY_MASK) == 0;
1023 if (!foundDisambig) {
1024 // Found concrete match, so return directly
1025 return foundResolve;
1028 // Accept any package from prefer list, otherwise first system app
1029 ResolveInfo firstSystem = null;
1030 for (ResolveInfo info : matches) {
1031 final boolean isSystem = (info.activityInfo.applicationInfo.flags
1032 & ApplicationInfo.FLAG_SYSTEM) != 0;
1033 final boolean isPrefer = QuickContactWindow.sPreferResolve
1034 .contains(info.activityInfo.applicationInfo.packageName);
1038 if (isPrefer) return info;
1039 if (isSystem && firstSystem != null) firstSystem = info;
1042 // Return first system found, otherwise first from list
1043 return firstSystem != null ? firstSystem : matches.get(0);
1047 * Check {@link PackageManager} to see if any apps offer to handle the
1048 * given {@link Action}.
1050 public boolean hasResolve(Action action) {
1051 return getEntry(action).bestResolve != null;
1055 * Find the best description for the given {@link Action}, usually used
1056 * for accessibility purposes.
1058 public CharSequence getDescription(Action action) {
1059 final CharSequence actionHeader = action.getHeader();
1060 final ResolveInfo info = getEntry(action).bestResolve;
1061 if (!TextUtils.isEmpty(actionHeader)) {
1062 return actionHeader;
1063 } else if (info != null) {
1064 return info.loadLabel(mPackageManager);
1071 * Return the best icon for the given {@link Action}, which is usually
1072 * based on the {@link ResolveInfo} found through a
1073 * {@link PackageManager} query.
1075 public Drawable getIcon(Action action) {
1076 final SoftReference<Drawable> iconRef = getEntry(action).icon;
1077 return (iconRef == null) ? null : iconRef.get();
1080 public void clear() {
1086 * Provide a strongly-typed {@link LinkedList} that holds a list of
1087 * {@link Action} objects.
1089 private class ActionList extends ArrayList<Action> {
1093 * Provide a simple way of collecting one or more {@link Action} objects
1094 * under a MIME-type key.
1096 private class ActionMap extends HashMap<String, ActionList> {
1097 private void collect(String mimeType, Action info) {
1098 // Create list for this MIME-type when needed
1099 ActionList collectList = get(mimeType);
1100 if (collectList == null) {
1101 collectList = new ActionList();
1102 put(mimeType, collectList);
1104 collectList.add(info);
1109 * Check if the given MIME-type appears in the list of excluded MIME-types
1110 * that the most-recent caller requested.
1112 private boolean isMimeExcluded(String mimeType) {
1113 if (mExcludeMimes == null) return false;
1114 for (String excludedMime : mExcludeMimes) {
1115 if (TextUtils.equals(excludedMime, mimeType)) {
1123 * Handle the result from the {@link #TOKEN_DATA} query.
1125 private void handleData(Cursor cursor) {
1126 if (cursor == null) return;
1128 if (!isMimeExcluded(Contacts.CONTENT_ITEM_TYPE)) {
1129 // Add the profile shortcut action
1130 final Action action = new ProfileAction(mContext, mLookupUri);
1131 mActions.collect(Contacts.CONTENT_ITEM_TYPE, action);
1134 final DataStatus status = new DataStatus();
1135 final Sources sources = Sources.getInstance(mContext);
1136 final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo);
1138 Bitmap photoBitmap = null;
1139 while (cursor.moveToNext()) {
1140 final long dataId = cursor.getLong(DataQuery._ID);
1141 final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
1142 final String mimeType = cursor.getString(DataQuery.MIMETYPE);
1144 // Handle any social status updates from this row
1145 status.possibleUpdate(cursor);
1147 // Skip this data item if MIME-type excluded
1148 if (isMimeExcluded(mimeType)) continue;
1150 // Handle photos included as data row
1151 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
1152 final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
1153 final byte[] photoBlob = cursor.getBlob(colPhoto);
1154 if (photoBlob != null) {
1155 photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
1160 final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
1161 ContactsSource.LEVEL_MIMETYPES);
1164 // Build an action for this data entry, find a mapping to a UI
1165 // element, build its summary from the cursor, and collect it
1166 // along with all others of this MIME-type.
1167 final Action action = new DataAction(mContext, mimeType, kind, dataId, cursor);
1168 considerAdd(action, mimeType);
1171 // If phone number, also insert as text message action
1172 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && kind != null) {
1173 final Action action = new DataAction(mContext, Constants.MIME_SMS_ADDRESS,
1174 kind, dataId, cursor);
1175 considerAdd(action, Constants.MIME_SMS_ADDRESS);
1178 // Handle Email rows with presence data as Im entry
1179 final boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE);
1180 if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
1181 final DataKind imKind = sources.getKindOrFallback(accountType,
1182 Im.CONTENT_ITEM_TYPE, mContext, ContactsSource.LEVEL_MIMETYPES);
1183 if (imKind != null) {
1184 final Action action = new DataAction(mContext, Im.CONTENT_ITEM_TYPE, imKind,
1186 considerAdd(action, Im.CONTENT_ITEM_TYPE);
1191 if (cursor.moveToLast()) {
1192 // Read contact information from last data row
1193 final String name = cursor.getString(DataQuery.DISPLAY_NAME);
1194 final int presence = cursor.getInt(DataQuery.CONTACT_PRESENCE);
1195 final Drawable statusIcon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence);
1197 setHeaderText(R.id.name, name);
1198 setHeaderImage(R.id.presence, statusIcon);
1201 if (photoView != null) {
1202 // Place photo when discovered in data, otherwise hide
1203 photoView.setVisibility(photoBitmap != null ? View.VISIBLE : View.GONE);
1204 photoView.setImageBitmap(photoBitmap);
1207 mHasValidSocial = status.isValid();
1208 if (mHasValidSocial && mMode != QuickContact.MODE_SMALL) {
1209 // Update status when valid was found
1210 setHeaderText(R.id.status, status.getStatus());
1211 setHeaderText(R.id.timestamp, status.getTimestampLabel(mContext));
1214 // Turn our list of actions into UI elements
1216 // Index where we start adding child views.
1217 int index = mTrack.getChildCount() - 1;
1219 // All the mime-types to add.
1220 final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
1222 // First, add PRECEDING_MIMETYPES, which are most common.
1223 for (String mimeType : PRECEDING_MIMETYPES) {
1224 if (containedTypes.contains(mimeType)) {
1225 mTrack.addView(inflateAction(mimeType), index++);
1226 containedTypes.remove(mimeType);
1230 // Keep the current index to append non PRECEDING/FOLLOWING items.
1231 final int indexAfterPreceding = index;
1233 // Then, add FOLLOWING_MIMETYPES, which are least common.
1234 for (String mimeType : FOLLOWING_MIMETYPES) {
1235 if (containedTypes.contains(mimeType)) {
1236 mTrack.addView(inflateAction(mimeType), index++);
1237 containedTypes.remove(mimeType);
1241 // Go back to just after PRECEDING_MIMETYPES, and append the rest.
1242 index = indexAfterPreceding;
1243 final String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
1244 Arrays.sort(remainingTypes);
1245 for (String mimeType : remainingTypes) {
1246 mTrack.addView(inflateAction(mimeType), index++);
1251 * Consider adding the given {@link Action}, which will only happen if
1252 * {@link PackageManager} finds an application to handle
1253 * {@link Action#getIntent()}.
1255 private void considerAdd(Action action, String mimeType) {
1256 if (mResolveCache.hasResolve(action)) {
1257 mActions.collect(mimeType, action);
1262 * Obtain a new {@link CheckableImageView} for a new chiclet, either by
1263 * recycling one from {@link #mActionPool}, or by inflating a new one. When
1264 * finished, use {@link #releaseView(View)} to return back into the pool for
1267 private synchronized View obtainView() {
1268 View view = mActionPool.poll();
1269 if (view == null || QuickContactActivity.FORCE_CREATE) {
1270 view = mInflater.inflate(R.layout.quickcontact_item, mTrack, false);
1276 * Return the given {@link CheckableImageView} into our internal pool for
1277 * possible recycling during another pass.
1279 private synchronized void releaseView(View view) {
1280 mActionPool.offer(view);
1285 * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
1286 * Will use the icon provided by the {@link DataKind}.
1288 private View inflateAction(String mimeType) {
1289 final CheckableImageView view = (CheckableImageView)obtainView();
1290 boolean isActionSet = false;
1292 // Add direct intent if single child, otherwise flag for multiple
1293 ActionList children = mActions.get(mimeType);
1294 if (children.size() > 1) {
1295 Collapser.collapseList(children);
1297 Action firstInfo = children.get(0);
1298 if (children.size() == 1) {
1299 view.setTag(firstInfo);
1301 for (Action action : children) {
1302 if (action.isPrimary()) {
1303 view.setTag(action);
1309 view.setTag(children);
1313 // Set icon and listen for clicks
1314 final CharSequence descrip = mResolveCache.getDescription(firstInfo);
1315 final Drawable icon = mResolveCache.getIcon(firstInfo);
1316 view.setChecked(false);
1317 view.setContentDescription(descrip);
1318 view.setImageDrawable(icon);
1319 view.setOnClickListener(this);
1323 /** {@inheritDoc} */
1324 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1325 // Pass list item clicks along so that Intents are handled uniformly
1330 * Flag indicating if {@link #mArrowDown} was visible during the last call
1331 * to {@link #setResolveVisible(boolean, CheckableImageView)}. Used to
1332 * decide during a later call if the arrow should be restored.
1334 private boolean mWasDownArrow = false;
1337 * Helper for showing and hiding {@link #mFooterDisambig}, which will
1338 * correctly manage {@link #mArrowDown} as needed.
1340 private void setResolveVisible(boolean visible, CheckableImageView actionView) {
1341 // Show or hide the resolve list if needed
1342 boolean visibleNow = mFooterDisambig.getVisibility() == View.VISIBLE;
1344 if (mLastAction != null) mLastAction.setChecked(false);
1345 if (actionView != null) actionView.setChecked(true);
1346 mLastAction = actionView;
1348 // Bail early if already in desired state
1349 if (visible == visibleNow) return;
1351 mFooter.setVisibility(visible ? View.GONE : View.VISIBLE);
1352 mFooterDisambig.setVisibility(visible ? View.VISIBLE : View.GONE);
1355 // If showing list, then hide and save state of down arrow
1356 mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE);
1357 mArrowDown.setVisibility(View.INVISIBLE);
1359 // If hiding list, restore any down arrow state
1360 mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE);
1364 /** {@inheritDoc} */
1365 public void onClick(View view) {
1366 final boolean isActionView = (view instanceof CheckableImageView);
1367 final CheckableImageView actionView = isActionView ? (CheckableImageView)view : null;
1368 final Object tag = view.getTag();
1369 if (tag instanceof Action) {
1370 // Incoming tag is concrete intent, so try launching
1371 final Action action = (Action)tag;
1372 final boolean makePrimary = mMakePrimary;
1375 mContext.startActivity(action.getIntent());
1376 } catch (ActivityNotFoundException e) {
1377 Toast.makeText(mContext, R.string.quickcontact_missing_app, Toast.LENGTH_SHORT)
1381 // Hide the resolution list, if present
1382 setResolveVisible(false, null);
1386 ContentValues values = new ContentValues(1);
1387 values.put(Data.IS_SUPER_PRIMARY, 1);
1388 final Uri dataUri = action.getDataUri();
1389 if (dataUri != null) {
1390 mContext.getContentResolver().update(dataUri, values, null, null);
1393 } else if (tag instanceof ActionList) {
1394 // Incoming tag is a MIME-type, so show resolution list
1395 final ActionList children = (ActionList)tag;
1397 // Show resolution list and set adapter
1398 setResolveVisible(true, actionView);
1400 mResolveList.setOnItemClickListener(this);
1401 mResolveList.setAdapter(new BaseAdapter() {
1402 public int getCount() {
1403 return children.size();
1406 public Object getItem(int position) {
1407 return children.get(position);
1410 public long getItemId(int position) {
1414 public View getView(int position, View convertView, ViewGroup parent) {
1415 if (convertView == null) {
1416 convertView = mInflater.inflate(
1417 R.layout.quickcontact_resolve_item, parent, false);
1420 // Set action title based on summary value
1421 final Action action = (Action)getItem(position);
1423 TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
1424 TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
1426 text1.setText(action.getHeader());
1427 text2.setText(action.getBody());
1429 convertView.setTag(action);
1434 // Make sure we resize to make room for ListView
1435 mDecor.forceLayout();
1436 mDecor.invalidate();
1441 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
1442 mMakePrimary = isChecked;
1445 private void onBackPressed() {
1446 // Back key will first dismiss any expanded resolve list, otherwise
1447 // it will close the entire dialog.
1448 if (mFooterDisambig.getVisibility() == View.VISIBLE) {
1449 setResolveVisible(false, null);
1450 mDecor.forceLayout();
1451 mDecor.invalidate();
1457 /** {@inheritDoc} */
1458 public boolean dispatchKeyEvent(KeyEvent event) {
1459 if (mWindow.superDispatchKeyEvent(event)) {
1462 return event.dispatch(this, mDecor != null
1463 ? mDecor.getKeyDispatcherState() : null, this);
1466 /** {@inheritDoc} */
1467 public boolean onKeyDown(int keyCode, KeyEvent event) {
1468 if (keyCode == KeyEvent.KEYCODE_BACK) {
1469 event.startTracking();
1476 /** {@inheritDoc} */
1477 public boolean onKeyUp(int keyCode, KeyEvent event) {
1478 if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
1479 && !event.isCanceled()) {
1487 /** {@inheritDoc} */
1488 public boolean onKeyLongPress(int keyCode, KeyEvent event) {
1492 /** {@inheritDoc} */
1493 public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
1497 /** {@inheritDoc} */
1498 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
1499 // TODO: make this window accessible
1504 * Detect if the given {@link MotionEvent} is outside the boundaries of this
1505 * window, which usually means we should dismiss.
1507 protected void detectEventOutside(MotionEvent event) {
1508 if (event.getAction() == MotionEvent.ACTION_DOWN && mDecor != null) {
1509 // Only try detecting outside events on down-press
1510 mDecor.getHitRect(mRect);
1511 mRect.top = mRect.top + mShadowTouch;
1512 mRect.bottom = mRect.bottom - mShadowTouch;
1513 final int x = (int)event.getX();
1514 final int y = (int)event.getY();
1515 if (!mRect.contains(x, y)) {
1516 event.setAction(MotionEvent.ACTION_OUTSIDE);
1521 /** {@inheritDoc} */
1522 public boolean dispatchTouchEvent(MotionEvent event) {
1523 detectEventOutside(event);
1524 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
1528 return mWindow.superDispatchTouchEvent(event);
1532 /** {@inheritDoc} */
1533 public boolean dispatchTrackballEvent(MotionEvent event) {
1534 return mWindow.superDispatchTrackballEvent(event);
1537 /** {@inheritDoc} */
1538 public void onContentChanged() {
1541 /** {@inheritDoc} */
1542 public boolean onCreatePanelMenu(int featureId, Menu menu) {
1546 /** {@inheritDoc} */
1547 public View onCreatePanelView(int featureId) {
1551 /** {@inheritDoc} */
1552 public boolean onMenuItemSelected(int featureId, MenuItem item) {
1556 /** {@inheritDoc} */
1557 public boolean onMenuOpened(int featureId, Menu menu) {
1561 /** {@inheritDoc} */
1562 public void onPanelClosed(int featureId, Menu menu) {
1565 /** {@inheritDoc} */
1566 public boolean onPreparePanel(int featureId, View view, Menu menu) {
1570 /** {@inheritDoc} */
1571 public boolean onSearchRequested() {
1575 /** {@inheritDoc} */
1576 public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
1577 if (mDecor != null) {
1578 mWindowManager.updateViewLayout(mDecor, attrs);
1582 /** {@inheritDoc} */
1583 public void onWindowFocusChanged(boolean hasFocus) {
1586 /** {@inheritDoc} */
1587 public void onAttachedToWindow() {
1591 /** {@inheritDoc} */
1592 public void onDetachedFromWindow() {
1596 private interface DataQuery {
1597 final String[] PROJECTION = new String[] {
1600 RawContacts.ACCOUNT_TYPE,
1602 Contacts.DISPLAY_NAME,
1603 Contacts.CONTACT_PRESENCE,
1606 Data.STATUS_RES_PACKAGE,
1609 Data.STATUS_TIMESTAMP,
1615 Data.IS_SUPER_PRIMARY,
1616 Data.RAW_CONTACT_ID,
1618 Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
1619 Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11,
1620 Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
1625 final int ACCOUNT_TYPE = 1;
1626 final int STARRED = 2;
1627 final int DISPLAY_NAME = 3;
1628 final int CONTACT_PRESENCE = 4;
1630 final int STATUS = 5;
1631 final int STATUS_RES_PACKAGE = 6;
1632 final int STATUS_ICON = 7;
1633 final int STATUS_LABEL = 8;
1634 final int STATUS_TIMESTAMP = 9;
1635 final int PRESENCE = 10;
1637 final int RES_PACKAGE = 11;
1638 final int MIMETYPE = 12;
1639 final int IS_PRIMARY = 13;
1640 final int IS_SUPER_PRIMARY = 14;