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.ContactsUtils;
20 import com.android.contacts.R;
21 import com.android.contacts.model.ContactsSource;
22 import com.android.contacts.model.Sources;
23 import com.android.contacts.model.ContactsSource.DataKind;
24 import com.android.contacts.ui.widget.CheckableImageView;
25 import com.android.contacts.util.Constants;
26 import com.android.contacts.util.NotifyingAsyncQueryHandler;
27 import com.android.internal.policy.PolicyManager;
28 import com.google.android.collect.Lists;
29 import com.google.android.collect.Sets;
31 import android.content.ActivityNotFoundException;
32 import android.content.ContentValues;
33 import android.content.ContentUris;
34 import android.content.Context;
35 import android.content.EntityIterator;
36 import android.content.Intent;
37 import android.content.pm.ApplicationInfo;
38 import android.content.pm.PackageManager;
39 import android.content.pm.ResolveInfo;
40 import android.content.res.Resources;
41 import android.database.Cursor;
42 import android.graphics.Bitmap;
43 import android.graphics.BitmapFactory;
44 import android.graphics.Rect;
45 import android.graphics.drawable.Drawable;
46 import android.net.Uri;
47 import android.os.Handler;
48 import android.provider.ContactsContract.Contacts;
49 import android.provider.ContactsContract.Data;
50 import android.provider.ContactsContract.QuickContact;
51 import android.provider.ContactsContract.RawContacts;
52 import android.provider.ContactsContract.StatusUpdates;
53 import android.provider.ContactsContract.CommonDataKinds.Email;
54 import android.provider.ContactsContract.CommonDataKinds.Im;
55 import android.provider.ContactsContract.CommonDataKinds.Phone;
56 import android.provider.ContactsContract.CommonDataKinds.Photo;
57 import android.provider.SocialContract.Activities;
58 import android.text.TextUtils;
59 import android.text.format.DateUtils;
60 import android.util.Log;
61 import android.util.Pool;
62 import android.util.Poolable;
63 import android.util.PoolableManager;
64 import android.util.Pools;
65 import android.view.ContextThemeWrapper;
66 import android.view.Gravity;
67 import android.view.KeyEvent;
68 import android.view.LayoutInflater;
69 import android.view.Menu;
70 import android.view.MenuItem;
71 import android.view.MotionEvent;
72 import android.view.VelocityTracker;
73 import android.view.View;
74 import android.view.ViewGroup;
75 import android.view.ViewStub;
76 import android.view.Window;
77 import android.view.WindowManager;
78 import android.view.Window.Callback;
79 import android.view.accessibility.AccessibilityEvent;
80 import android.view.animation.Animation;
81 import android.view.animation.AnimationUtils;
82 import android.view.animation.Interpolator;
83 import android.widget.AbsListView;
84 import android.widget.AdapterView;
85 import android.widget.BaseAdapter;
86 import android.widget.CheckBox;
87 import android.widget.CompoundButton;
88 import android.widget.HorizontalScrollView;
89 import android.widget.ImageView;
90 import android.widget.ListView;
91 import android.widget.TextView;
92 import android.widget.Toast;
94 import java.lang.ref.SoftReference;
95 import java.util.ArrayList;
96 import java.util.Arrays;
97 import java.util.HashMap;
98 import java.util.HashSet;
99 import java.util.LinkedList;
100 import java.util.List;
101 import java.util.Set;
104 * Window that shows QuickContact dialog for a specific {@link Contacts#_ID}.
106 public class QuickContactWindow implements Window.Callback,
107 NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener,
108 AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback {
109 private static final String TAG = "QuickContactWindow";
112 * Interface used to allow the person showing a {@link QuickContactWindow} to
113 * know when the window has been dismissed.
115 public interface OnDismissListener {
116 public void onDismiss(QuickContactWindow dialog);
119 private final Context mContext;
120 private final LayoutInflater mInflater;
121 private final WindowManager mWindowManager;
122 private Window mWindow;
124 private final Rect mRect = new Rect();
126 private boolean mQuerying = false;
127 private boolean mShowing = false;
129 private NotifyingAsyncQueryHandler mHandler;
130 private OnDismissListener mDismissListener;
131 private ResolveCache mResolveCache;
133 private Uri mLookupUri;
134 private Rect mAnchor;
136 private int mShadowHeight;
138 private boolean mHasValidSocial = false;
139 private boolean mHasData = false;
140 private boolean mMakePrimary = false;
142 private ImageView mArrowUp;
143 private ImageView mArrowDown;
146 private View mHeader;
147 private HorizontalScrollView mTrackScroll;
148 private ViewGroup mTrack;
149 private Animation mTrackAnim;
151 private View mFooter;
152 private View mFooterDisambig;
153 private ListView mResolveList;
154 private CheckableImageView mLastAction;
155 private CheckBox mSetPrimaryCheckBox;
157 private int mWindowRecycled = 0;
158 private int mActionRecycled = 0;
161 * Set of {@link Action} that are associated with the aggregate currently
162 * displayed by this dialog, represented as a map from {@link String}
163 * MIME-type to {@link ActionList}.
165 private ActionMap mActions = new ActionMap();
168 * Pool of unused {@link CheckableImageView} that have previously been
169 * inflated, and are ready to be recycled through {@link #obtainView()}.
171 private LinkedList<View> mActionPool = new LinkedList<View>();
173 private String[] mExcludeMimes;
176 * Specific MIME-types that should be bumped to the front of the dialog.
177 * Other MIME-types not appearing in this list follow in alphabetic order.
179 private static final String[] ORDERED_MIMETYPES = new String[] {
180 Phone.CONTENT_ITEM_TYPE,
181 Contacts.CONTENT_ITEM_TYPE,
182 Constants.MIME_SMS_ADDRESS,
183 Email.CONTENT_ITEM_TYPE,
187 * Specific list {@link ApplicationInfo#packageName} of apps that are
188 * prefered <strong>only</strong> for the purposes of default icons when
189 * multiple {@link ResolveInfo} are found to match. This only happens when
190 * the user has not selected a default app yet, and they will still be
191 * presented with the system disambiguation dialog.
193 private static final HashSet<String> sPreferResolve = Sets.newHashSet(new String[] {
195 "com.android.calendar",
196 "com.android.contacts",
201 private static final int TOKEN_DATA = 1;
203 static final boolean LOGD = false;
205 static final boolean TRACE_LAUNCH = false;
206 static final String TRACE_TAG = "quickcontact";
209 * Prepare a dialog to show in the given {@link Context}.
211 public QuickContactWindow(Context context) {
212 mContext = new ContextThemeWrapper(context, R.style.QuickContact);
213 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
214 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
216 mWindow = PolicyManager.makeNewWindow(mContext);
217 mWindow.setCallback(this);
218 mWindow.setWindowManager(mWindowManager, null, null);
220 mWindow.setContentView(R.layout.quickcontact);
222 mArrowUp = (ImageView)mWindow.findViewById(R.id.arrow_up);
223 mArrowDown = (ImageView)mWindow.findViewById(R.id.arrow_down);
225 mResolveCache = new ResolveCache(mContext);
227 final Resources res = mContext.getResources();
228 mShadowHeight = res.getDimensionPixelSize(R.dimen.quickcontact_shadow);
230 mTrack = (ViewGroup)mWindow.findViewById(R.id.quickcontact);
231 mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
233 mFooter = mWindow.findViewById(R.id.footer);
234 mFooterDisambig = mWindow.findViewById(R.id.footer_disambig);
235 mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
236 mSetPrimaryCheckBox = (CheckBox)mWindow.findViewById(android.R.id.checkbox);
238 mSetPrimaryCheckBox.setOnCheckedChangeListener(this);
240 // Prepare track entrance animation
241 mTrackAnim = AnimationUtils.loadAnimation(mContext, R.anim.quickcontact);
242 mTrackAnim.setInterpolator(new Interpolator() {
243 public float getInterpolation(float t) {
244 // Pushes past the target area, then snaps back into place.
245 // Equation for graphing: 1.2-((x*1.6)-1.1)^2
246 final float inner = (t * 1.55f) - 1.1f;
247 return 1.2f - inner * inner;
251 mHandler = new NotifyingAsyncQueryHandler(mContext, this);
255 * Prepare a dialog to show in the given {@link Context}, and notify the
256 * given {@link OnDismissListener} each time this dialog is dismissed.
258 public QuickContactWindow(Context context, OnDismissListener dismissListener) {
260 mDismissListener = dismissListener;
263 private View getHeaderView(int mode) {
266 case QuickContact.MODE_SMALL:
267 header = mWindow.findViewById(R.id.header_small);
269 case QuickContact.MODE_MEDIUM:
270 header = mWindow.findViewById(R.id.header_medium);
272 case QuickContact.MODE_LARGE:
273 header = mWindow.findViewById(R.id.header_large);
277 if (header instanceof ViewStub) {
278 // Inflate actual header if we picked a stub
279 final ViewStub stub = (ViewStub)header;
280 header = stub.inflate();
282 header.setVisibility(View.VISIBLE);
289 * Start showing a dialog for the given {@link Contacts#_ID} pointing
290 * towards the given location.
292 public void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
293 if (mShowing || mQuerying) {
294 Log.w(TAG, "already in process of showing");
298 if (TRACE_LAUNCH && !android.os.Debug.isMethodTracingActive()) {
299 android.os.Debug.startMethodTracing(TRACE_TAG);
302 // Prepare header view for requested mode
303 mLookupUri = lookupUri;
304 mAnchor = new Rect(anchor);
306 mExcludeMimes = excludeMimes;
308 mHeader = getHeaderView(mode);
310 setHeaderText(R.id.name, R.string.quickcontact_missing_name);
312 setHeaderText(R.id.status, null);
313 setHeaderText(R.id.timestamp, null);
315 setHeaderImage(R.id.presence, null);
316 setHeaderImage(R.id.source, null);
318 mHasValidSocial = false;
321 // Start background query for data, but only select photo rows when they
322 // directly match the super-primary PHOTO_ID.
323 final Uri dataUri = getDataUri(lookupUri);
324 mHandler.cancelOperation(TOKEN_DATA);
326 // Only request photo data when required by mode
327 if (mMode == QuickContact.MODE_LARGE) {
328 // Select photos, but only super-primary
329 mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
330 + "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
331 + ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
333 // Exclude all photos from cursor
334 mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
335 + "!=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
340 * Build a {@link Uri} into the {@link Data} table for the requested
341 * {@link Contacts#CONTENT_LOOKUP_URI} style {@link Uri}.
343 private Uri getDataUri(Uri lookupUri) {
344 // TODO: Formalize method of extracting LOOKUP_KEY
345 final List<String> path = lookupUri.getPathSegments();
346 final boolean validLookup = path.size() >= 3 && "lookup".equals(path.get(1));
348 // We only accept valid lookup-style Uris
349 throw new IllegalArgumentException("Expecting lookup-style Uri");
350 } else if (path.size() == 3) {
351 // No direct _ID provided, so force a lookup
352 lookupUri = Contacts.lookupContact(mContext.getContentResolver(), lookupUri);
355 final long contactId = ContentUris.parseId(lookupUri);
356 return Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
357 Contacts.Data.CONTENT_DIRECTORY);
361 * Show the correct call-out arrow based on a {@link R.id} reference.
363 private void showArrow(int whichArrow, int requestedX) {
364 final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
365 final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
367 final int arrowWidth = mArrowUp.getMeasuredWidth();
369 showArrow.setVisibility(View.VISIBLE);
370 ViewGroup.MarginLayoutParams param = (ViewGroup.MarginLayoutParams)showArrow.getLayoutParams();
371 param.leftMargin = requestedX - arrowWidth / 2;
373 hideArrow.setVisibility(View.INVISIBLE);
377 * Actual internal method to show this dialog. Called only by
378 * {@link #considerShowing()} when all data requirements have been met.
380 private void showInternal() {
381 mDecor = mWindow.getDecorView();
382 WindowManager.LayoutParams l = mWindow.getAttributes();
384 l.width = WindowManager.LayoutParams.FILL_PARENT;
385 l.height = WindowManager.LayoutParams.WRAP_CONTENT;
387 // Force layout measuring pass so we have baseline numbers
388 mDecor.measure(l.width, l.height);
390 final int blockHeight = mDecor.getMeasuredHeight();
392 l.gravity = Gravity.TOP | Gravity.LEFT;
395 if (mAnchor.top > blockHeight) {
396 // Show downwards callout when enough room, aligning bottom block
397 // edge with top of anchor area, and adjusting to inset arrow.
398 showArrow(R.id.arrow_down, mAnchor.centerX());
399 l.y = mAnchor.top - blockHeight + mShadowHeight;
400 l.windowAnimations = R.style.QuickContactAboveAnimation;
403 // Otherwise show upwards callout, aligning block top with bottom of
404 // anchor area, and adjusting to inset arrow.
405 showArrow(R.id.arrow_up, mAnchor.centerX());
406 l.y = mAnchor.bottom - mShadowHeight;
407 l.windowAnimations = R.style.QuickContactBelowAnimation;
411 l.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
412 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
414 mWindowManager.addView(mDecor, l);
418 mTrack.startAnimation(mTrackAnim);
421 android.os.Debug.stopMethodTracing();
422 Log.d(TAG, "Window recycled " + mWindowRecycled + " times, chiclets "
423 + mActionRecycled + " times");
428 * Dismiss this dialog if showing.
430 public void dismiss() {
431 // Notify any listeners that we've been dismissed
432 if (mDismissListener != null) {
433 mDismissListener.onDismiss(this);
437 if (LOGD) Log.d(TAG, "not visible, ignore");
441 boolean hadDecor = mDecor != null;
443 mWindowManager.removeView(mDecor);
445 mWindow.closeAllPanels();
448 // Release reference to last chiclet.
451 // Completely hide header from current mode
452 mHeader.setVisibility(View.GONE);
454 // Cancel any pending queries
455 mHandler.cancelOperation(TOKEN_DATA);
457 // Clear track actions and scroll to hard left
458 mResolveCache.clear();
461 // Recycle any chiclets in use
462 while (mTrack.getChildCount() > 2) {
463 this.releaseView(mTrack.getChildAt(1));
464 mTrack.removeViewAt(1);
467 mTrackScroll.fullScroll(View.FOCUS_LEFT);
468 mWasDownArrow = false;
470 setResolveVisible(false, null);
474 if (!hadDecor || !mShowing) {
475 if (LOGD) Log.d(TAG, "not showing, ignore");
484 * Returns true if this dialog is showing or querying.
486 public boolean isShowing() {
487 return mShowing || mQuerying;
491 * Consider showing this window, which will only call through to
492 * {@link #showInternal()} when all data items are present.
494 private synchronized void considerShowing() {
495 if (mHasData && !mShowing) {
496 if (mMode == QuickContact.MODE_MEDIUM && !mHasValidSocial) {
497 // Missing valid social, swap medium for small header
498 mHeader.setVisibility(View.GONE);
499 mHeader = getHeaderView(QuickContact.MODE_SMALL);
502 // All queries have returned, pull curtain
508 public void onQueryComplete(int token, Object cookie, Cursor cursor) {
509 if (cursor == null) {
510 // Problem while running query, so bail without showing
511 Log.w(TAG, "Missing cursor for token=" + token);
519 if (!cursor.isClosed()) {
526 /** Assign this string to the view, if found in {@link #mHeader}. */
527 private void setHeaderText(int id, int resId) {
528 setHeaderText(id, mContext.getResources().getText(resId));
531 /** Assign this string to the view, if found in {@link #mHeader}. */
532 private void setHeaderText(int id, CharSequence value) {
533 final View view = mHeader.findViewById(id);
534 if (view instanceof TextView) {
535 ((TextView)view).setText(value);
536 view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE);
540 /** Assign this image to the view, if found in {@link #mHeader}. */
541 private void setHeaderImage(int id, int resId) {
542 setHeaderImage(id, mContext.getResources().getDrawable(resId));
545 /** Assign this image to the view, if found in {@link #mHeader}. */
546 private void setHeaderImage(int id, Drawable drawable) {
547 final View view = mHeader.findViewById(id);
548 if (view instanceof ImageView) {
549 ((ImageView)view).setImageDrawable(drawable);
550 view.setVisibility(drawable == null ? View.GONE : View.VISIBLE);
555 * Find the presence icon for showing in summary header.
557 private Drawable getPresenceIcon(int status) {
560 case StatusUpdates.AVAILABLE:
561 resId = android.R.drawable.presence_online;
563 case StatusUpdates.IDLE:
564 case StatusUpdates.AWAY:
565 resId = android.R.drawable.presence_away;
567 case StatusUpdates.DO_NOT_DISTURB:
568 resId = android.R.drawable.presence_busy;
572 return mContext.getResources().getDrawable(resId);
579 * Find the QuickContact-specific presence icon for showing in chiclets.
581 private Drawable getTrackPresenceIcon(int status) {
584 case StatusUpdates.AVAILABLE:
585 resId = R.drawable.quickcontact_slider_presence_active;
587 case StatusUpdates.IDLE:
588 case StatusUpdates.AWAY:
589 resId = R.drawable.quickcontact_slider_presence_away;
591 case StatusUpdates.DO_NOT_DISTURB:
592 resId = R.drawable.quickcontact_slider_presence_busy;
594 case StatusUpdates.INVISIBLE:
595 resId = R.drawable.quickcontact_slider_presence_inactive;
597 case StatusUpdates.OFFLINE:
599 resId = R.drawable.quickcontact_slider_presence_inactive;
601 return mContext.getResources().getDrawable(resId);
604 /** Read {@link String} from the given {@link Cursor}. */
605 private static String getAsString(Cursor cursor, String columnName) {
606 final int index = cursor.getColumnIndex(columnName);
607 return cursor.getString(index);
610 /** Read {@link Integer} from the given {@link Cursor}. */
611 private static int getAsInt(Cursor cursor, String columnName) {
612 final int index = cursor.getColumnIndex(columnName);
613 return cursor.getInt(index);
617 * Abstract definition of an action that could be performed, along with
618 * string description and icon.
620 private interface Action {
621 public CharSequence getHeader();
622 public CharSequence getBody();
624 public String getMimeType();
625 public Drawable getFallbackIcon();
628 * Build an {@link Intent} that will perform this action.
630 public Intent getIntent();
633 * Checks if the contact data for this action is primary.
635 public Boolean isPrimary();
638 * Returns a lookup (@link Uri) for the contact data item.
640 public Uri getDataUri();
644 * Description of a specific {@link Data#_ID} item, with style information
645 * defined by a {@link DataKind}.
647 private static class DataAction implements Action {
648 private final Context mContext;
649 private final DataKind mKind;
650 private final String mMimeType;
652 private CharSequence mHeader;
653 private CharSequence mBody;
654 private Intent mIntent;
656 private boolean mAlternate;
657 private Uri mDataUri;
658 private boolean mIsPrimary;
661 * Create an action from common {@link Data} elements.
663 public DataAction(Context context, String mimeType, DataKind kind,
664 long dataId, Cursor cursor) {
667 mMimeType = mimeType;
669 // Inflate strings from cursor
670 mAlternate = Constants.MIME_SMS_ADDRESS.equals(mimeType);
671 if (mAlternate && mKind.actionAltHeader != null) {
672 mHeader = mKind.actionAltHeader.inflateUsing(context, cursor);
673 } else if (mKind.actionHeader != null) {
674 mHeader = mKind.actionHeader.inflateUsing(context, cursor);
677 if (getAsInt(cursor, Data.IS_SUPER_PRIMARY) != 0) {
681 if (mKind.actionBody != null) {
682 mBody = mKind.actionBody.inflateUsing(context, cursor);
685 mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
687 // Handle well-known MIME-types with special care
688 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
689 final String number = getAsString(cursor, Phone.NUMBER);
690 if (!TextUtils.isEmpty(number)) {
691 final Uri callUri = Uri.fromParts(Constants.SCHEME_TEL, number, null);
692 mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
695 } else if (Constants.MIME_SMS_ADDRESS.equals(mimeType)) {
696 final String number = getAsString(cursor, Phone.NUMBER);
697 if (!TextUtils.isEmpty(number)) {
698 final Uri smsUri = Uri.fromParts(Constants.SCHEME_SMSTO, number, null);
699 mIntent = new Intent(Intent.ACTION_SENDTO, smsUri);
702 } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
703 final String address = getAsString(cursor, Email.DATA);
704 if (!TextUtils.isEmpty(address)) {
705 final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null);
706 mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
709 } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
710 final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(
711 getAsString(cursor, Data.MIMETYPE));
712 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
713 getAsInt(cursor, Im.PROTOCOL);
715 String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
716 String data = getAsString(cursor, isEmail ? Email.DATA : Im.DATA);
717 if (protocol != Im.PROTOCOL_CUSTOM) {
718 // Try bringing in a well-known host for specific protocols
719 host = ContactsUtils.lookupProviderNameFromId(protocol);
722 if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) {
723 final String authority = host.toLowerCase();
724 final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
725 authority).appendPath(data).build();
726 mIntent = new Intent(Intent.ACTION_SENDTO, imUri);
730 if (mIntent == null) {
731 // Otherwise fall back to default VIEW action
732 mIntent = new Intent(Intent.ACTION_VIEW, mDataUri);
735 // Always launch as new task, since we're like a launcher
736 mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
740 public CharSequence getHeader() {
745 public CharSequence getBody() {
750 public String getMimeType() {
755 public Uri getDataUri() {
760 public Boolean isPrimary() {
765 public Drawable getFallbackIcon() {
766 // Bail early if no valid resources
767 final String resPackageName = mKind.resPackageName;
768 if (resPackageName == null) return null;
770 final PackageManager pm = mContext.getPackageManager();
771 if (mAlternate && mKind.iconAltRes != -1) {
772 return pm.getDrawable(resPackageName, mKind.iconAltRes, null);
773 } else if (mKind.iconRes != -1) {
774 return pm.getDrawable(resPackageName, mKind.iconRes, null);
781 public Intent getIntent() {
787 * Specific action that launches the profile card.
789 private static class ProfileAction implements Action {
790 private final Context mContext;
791 private final Uri mLookupUri;
793 public ProfileAction(Context context, Uri lookupUri) {
795 mLookupUri = lookupUri;
799 public CharSequence getHeader() {
804 public CharSequence getBody() {
809 public String getMimeType() {
810 return Contacts.CONTENT_ITEM_TYPE;
814 public Drawable getFallbackIcon() {
815 return mContext.getResources().getDrawable(R.drawable.ic_contacts_details);
819 public Intent getIntent() {
820 final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
821 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
826 public Boolean isPrimary() {
831 public Uri getDataUri() {
838 * Internally hold a cache of scaled icons based on {@link PackageManager}
839 * queries, keyed internally on MIME-type.
841 private static class ResolveCache {
842 private Context mContext;
843 private PackageManager mPackageManager;
846 * Cached entry holding the best {@link ResolveInfo} for a specific
847 * MIME-type, along with a {@link SoftReference} to its icon.
849 private static class Entry {
850 public ResolveInfo bestResolve;
851 public SoftReference<Drawable> icon;
854 private HashMap<String, Entry> mCache = new HashMap<String, Entry>();
856 public ResolveCache(Context context) {
858 mPackageManager = context.getPackageManager();
862 * Get the {@link Entry} best associated with the given {@link Action},
863 * or create and populate a new one if it doesn't exist.
865 protected Entry getEntry(Action action) {
866 final String mimeType = action.getMimeType();
867 Entry entry = mCache.get(mimeType);
868 if (entry != null) return entry;
871 final Intent intent = action.getIntent();
872 if (intent != null) {
873 final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
874 PackageManager.MATCH_DEFAULT_ONLY);
876 // Pick first match, otherwise best found
877 ResolveInfo bestResolve = null;
878 final int size = matches.size();
880 bestResolve = matches.get(0);
881 } else if (size > 1) {
882 bestResolve = getBestResolve(matches);
885 if (bestResolve != null) {
886 final Drawable icon = bestResolve.loadIcon(mPackageManager);
888 entry.bestResolve = bestResolve;
889 entry.icon = new SoftReference<Drawable>(icon);
893 mCache.put(mimeType, entry);
898 * Best {@link ResolveInfo} when multiple found. Ties are broken by
899 * selecting first from the {QuickContactWindow#sPreferResolve} list of
900 * preferred packages, second by apps that live on the system partition,
901 * otherwise the app from the top of the list. This is
902 * <strong>only</strong> used for selecting a default icon for
903 * displaying in the track, and does not shortcut the system
904 * {@link Intent} disambiguation dialog.
906 protected ResolveInfo getBestResolve(List<ResolveInfo> matches) {
907 // Accept any package from prefer list, otherwise first system app
908 ResolveInfo firstSystem = null;
909 for (ResolveInfo info : matches) {
910 final boolean isSystem = (info.activityInfo.applicationInfo.flags
911 & ApplicationInfo.FLAG_SYSTEM) != 0;
912 final boolean isPrefer = QuickContactWindow.sPreferResolve
913 .contains(info.activityInfo.applicationInfo.packageName);
915 if (isPrefer) return info;
916 if (isSystem && firstSystem != null) firstSystem = info;
919 // Return first system found, otherwise first from list
920 return firstSystem != null ? firstSystem : matches.get(0);
924 * Check {@link PackageManager} to see if any apps offer to handle the
925 * given {@link Action}.
927 public boolean hasResolve(Action action) {
928 return getEntry(action).bestResolve != null;
932 * Find the best description for the given {@link Action}, usually used
933 * for accessibility purposes.
935 public CharSequence getDescription(Action action) {
936 final CharSequence actionHeader = action.getHeader();
937 final ResolveInfo info = getEntry(action).bestResolve;
938 if (!TextUtils.isEmpty(actionHeader)) {
940 } else if (info != null) {
941 return info.loadLabel(mPackageManager);
948 * Return the best icon for the given {@link Action}, which is usually
949 * based on the {@link ResolveInfo} found through a
950 * {@link PackageManager} query.
952 public Drawable getIcon(Action action) {
953 final SoftReference<Drawable> iconRef = getEntry(action).icon;
954 return (iconRef == null) ? null : iconRef.get();
957 public void clear() {
963 * Provide a strongly-typed {@link LinkedList} that holds a list of
964 * {@link Action} objects.
966 private class ActionList extends LinkedList<Action> {
970 * Provide a simple way of collecting one or more {@link Action} objects
971 * under a MIME-type key.
973 private class ActionMap extends HashMap<String, ActionList> {
974 private void collect(String mimeType, Action info) {
975 // Create list for this MIME-type when needed
976 ActionList collectList = get(mimeType);
977 if (collectList == null) {
978 collectList = new ActionList();
979 put(mimeType, collectList);
981 collectList.add(info);
986 * Check if the given MIME-type appears in the list of excluded MIME-types
987 * that the most-recent caller requested.
989 private boolean isMimeExcluded(String mimeType) {
990 if (mExcludeMimes == null) return false;
991 for (String excludedMime : mExcludeMimes) {
992 if (TextUtils.equals(excludedMime, mimeType)) {
1000 * Internal storage for the latest social status, as built when walking
1001 * across a {@Link DataQuery} query. Will always keep record of at
1002 * least the first status it encounters, but will replace it with newer
1003 * statuses, as determined by timestamps.
1005 private static class LatestStatus {
1006 private String mStatus = null;
1007 private long mTimestamp = -1;
1009 private String mResPackage = null;
1010 private int mIconRes = -1;
1011 private int mLabelRes = -1;
1013 private int getCursorInt(Cursor cursor, int columnIndex, int missingValue) {
1014 if (cursor.isNull(columnIndex)) return missingValue;
1015 return cursor.getInt(columnIndex);
1019 * Attempt updating this {@link LatestStatus} based on values at the
1020 * current row of the given {@link Cursor}. Assumes that query
1021 * projection was {@link DataQuery#PROJECTION}.
1023 public void possibleUpdate(Cursor cursor) {
1024 final boolean hasStatus = !cursor.isNull(DataQuery.STATUS);
1025 final boolean hasTimestamp = !cursor.isNull(DataQuery.STATUS_TIMESTAMP);
1027 // Bail early when not valid status, or when previous status was
1028 // found and we can't compare this one.
1029 if (!hasStatus) return;
1030 if (isValid() && !hasTimestamp) return;
1033 // Compare timestamps and bail if older status
1034 final long newTimestamp = cursor.getLong(DataQuery.STATUS_TIMESTAMP);
1035 if (newTimestamp < mTimestamp) return;
1037 mTimestamp = newTimestamp;
1040 // Fill in remaining details from cursor
1041 mStatus = cursor.getString(DataQuery.STATUS);
1042 mResPackage = cursor.getString(DataQuery.STATUS_RES_PACKAGE);
1043 mIconRes = getCursorInt(cursor, DataQuery.STATUS_ICON, -1);
1044 mLabelRes = getCursorInt(cursor, DataQuery.STATUS_LABEL, -1);
1047 public boolean isValid() {
1048 return !TextUtils.isEmpty(mStatus);
1051 public CharSequence getStatus() {
1056 * Build any timestamp and label into a single string.
1058 public CharSequence getTimestampLabel(Context context) {
1059 final PackageManager pm = context.getPackageManager();
1061 final boolean validTimestamp = mTimestamp > 0;
1062 final boolean validLabel = mResPackage != null && mLabelRes != -1;
1064 final CharSequence timeClause = validTimestamp ? DateUtils.getRelativeTimeSpanString(
1065 mTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
1066 DateUtils.FORMAT_ABBREV_RELATIVE) : null;
1067 final CharSequence labelClause = validLabel ? pm.getText(mResPackage, mLabelRes,
1070 if (validTimestamp && validLabel) {
1071 return context.getString(
1072 com.android.internal.R.string.contact_status_update_attribution_with_date,
1073 timeClause, labelClause);
1074 } else if (validLabel) {
1075 return context.getString(
1076 com.android.internal.R.string.contact_status_update_attribution,
1078 } else if (validTimestamp) {
1085 public Drawable getIcon(Context context) {
1086 final PackageManager pm = context.getPackageManager();
1087 final boolean validIcon = mResPackage != null && mIconRes != -1;
1088 return validIcon ? pm.getDrawable(mResPackage, mIconRes, null) : null;
1093 * Handle the result from the {@link #TOKEN_DATA} query.
1095 private void handleData(Cursor cursor) {
1096 if (cursor == null) return;
1098 if (!isMimeExcluded(Contacts.CONTENT_ITEM_TYPE)) {
1099 // Add the profile shortcut action
1100 final Action action = new ProfileAction(mContext, mLookupUri);
1101 mActions.collect(Contacts.CONTENT_ITEM_TYPE, action);
1104 final LatestStatus status = new LatestStatus();
1105 final Sources sources = Sources.getInstance(mContext);
1106 final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo);
1108 Bitmap photoBitmap = null;
1109 while (cursor.moveToNext()) {
1110 final long dataId = cursor.getLong(DataQuery._ID);
1111 final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
1112 final String resPackage = cursor.getString(DataQuery.RES_PACKAGE);
1113 final String mimeType = cursor.getString(DataQuery.MIMETYPE);
1115 // Handle any social status updates from this row
1116 status.possibleUpdate(cursor);
1118 // Skip this data item if MIME-type excluded
1119 if (isMimeExcluded(mimeType)) continue;
1121 // Handle photos included as data row
1122 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
1123 final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
1124 final byte[] photoBlob = cursor.getBlob(colPhoto);
1125 if (photoBlob != null) {
1126 photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
1131 final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
1132 ContactsSource.LEVEL_MIMETYPES);
1135 // Build an action for this data entry, find a mapping to a UI
1136 // element, build its summary from the cursor, and collect it
1137 // along with all others of this MIME-type.
1138 final Action action = new DataAction(mContext, mimeType, kind, dataId, cursor);
1139 considerAdd(action, mimeType);
1142 // If phone number, also insert as text message action
1143 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && kind != null) {
1144 final Action action = new DataAction(mContext, Constants.MIME_SMS_ADDRESS,
1145 kind, dataId, cursor);
1146 considerAdd(action, Constants.MIME_SMS_ADDRESS);
1149 // Handle Email rows with presence data as Im entry
1150 final boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE);
1151 if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
1152 final DataKind imKind = sources.getKindOrFallback(accountType,
1153 Im.CONTENT_ITEM_TYPE, mContext, ContactsSource.LEVEL_MIMETYPES);
1154 if (imKind != null) {
1155 final Action action = new DataAction(mContext, Im.CONTENT_ITEM_TYPE, imKind,
1157 considerAdd(action, Im.CONTENT_ITEM_TYPE);
1162 if (cursor.moveToLast()) {
1163 // Read contact information from last data row
1164 final String name = cursor.getString(DataQuery.DISPLAY_NAME);
1165 final int presence = cursor.getInt(DataQuery.CONTACT_PRESENCE);
1166 final Drawable statusIcon = getPresenceIcon(presence);
1168 setHeaderText(R.id.name, name);
1169 setHeaderImage(R.id.presence, statusIcon);
1172 if (photoView != null) {
1173 // Place photo when discovered in data, otherwise hide
1174 photoView.setVisibility(photoBitmap != null ? View.VISIBLE : View.GONE);
1175 photoView.setImageBitmap(photoBitmap);
1178 mHasValidSocial = status.isValid();
1179 if (mHasValidSocial && mMode != QuickContact.MODE_SMALL) {
1180 // Update status when valid was found
1181 setHeaderText(R.id.status, status.getStatus());
1182 setHeaderText(R.id.timestamp, status.getTimestampLabel(mContext));
1184 final Drawable icon = status.getIcon(mContext);
1185 setHeaderImage(R.id.source, icon);
1187 if (mMode == QuickContact.MODE_MEDIUM) {
1188 // Hide medium divider when missing icon
1189 final boolean validIcon = icon != null;
1190 mHeader.findViewById(R.id.source_divider).setVisibility(
1191 validIcon ? View.VISIBLE : View.GONE);
1195 // Turn our list of actions into UI elements, starting with common types
1196 final Set<String> containedTypes = mActions.keySet();
1197 for (String mimeType : ORDERED_MIMETYPES) {
1198 if (containedTypes.contains(mimeType)) {
1199 final int index = mTrack.getChildCount() - 1;
1200 mTrack.addView(inflateAction(mimeType), index);
1201 containedTypes.remove(mimeType);
1205 // Then continue with remaining MIME-types in alphabetical order
1206 final String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
1207 Arrays.sort(remainingTypes);
1208 for (String mimeType : remainingTypes) {
1209 final int index = mTrack.getChildCount() - 1;
1210 mTrack.addView(inflateAction(mimeType), index);
1215 * Consider adding the given {@link Action}, which will only happen if
1216 * {@link PackageManager} finds an application to handle
1217 * {@link Action#getIntent()}.
1219 private void considerAdd(Action action, String mimeType) {
1220 if (mResolveCache.hasResolve(action)) {
1221 mActions.collect(mimeType, action);
1226 * Obtain a new {@link CheckableImageView} for a new chiclet, either by
1227 * recycling one from {@link #mActionPool}, or by inflating a new one. When
1228 * finished, use {@link #releaseView(View)} to return back into the pool for
1231 private synchronized View obtainView() {
1232 View view = mActionPool.poll();
1233 if (view == null || QuickContactActivity.FORCE_CREATE) {
1234 view = mInflater.inflate(R.layout.quickcontact_item, mTrack, false);
1240 * Return the given {@link CheckableImageView} into our internal pool for
1241 * possible recycling during another pass.
1243 private synchronized void releaseView(View view) {
1244 mActionPool.offer(view);
1249 * Inflate the in-track view for the action of the given MIME-type. Will use
1250 * the icon provided by the {@link DataKind}.
1252 private View inflateAction(String mimeType) {
1253 final CheckableImageView view = (CheckableImageView)obtainView();
1254 boolean isActionSet = false;
1256 // Add direct intent if single child, otherwise flag for multiple
1257 ActionList children = mActions.get(mimeType);
1258 Action firstInfo = children.get(0);
1259 if (children.size() == 1) {
1260 view.setTag(firstInfo);
1262 for (Action action : children) {
1263 if (action.isPrimary()) {
1264 view.setTag(action);
1270 view.setTag(children);
1274 // Set icon and listen for clicks
1275 final CharSequence descrip = mResolveCache.getDescription(firstInfo);
1276 final Drawable icon = mResolveCache.getIcon(firstInfo);
1277 view.setChecked(false);
1278 view.setContentDescription(descrip);
1279 view.setImageDrawable(icon);
1280 view.setOnClickListener(this);
1284 /** {@inheritDoc} */
1285 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1286 // Pass list item clicks along so that Intents are handled uniformly
1291 * Flag indicating if {@link #mArrowDown} was visible during the last call
1292 * to {@link #setResolveVisible(boolean, CheckableImageView)}. Used to
1293 * decide during a later call if the arrow should be restored.
1295 private boolean mWasDownArrow = false;
1298 * Helper for showing and hiding {@link #mFooterDisambig}, which will
1299 * correctly manage {@link #mArrowDown} as needed.
1301 private void setResolveVisible(boolean visible, CheckableImageView actionView) {
1302 // Show or hide the resolve list if needed
1303 boolean visibleNow = mFooterDisambig.getVisibility() == View.VISIBLE;
1305 if (mLastAction != null) mLastAction.setChecked(false);
1306 if (actionView != null) actionView.setChecked(true);
1307 mLastAction = actionView;
1309 // Bail early if already in desired state
1310 if (visible == visibleNow) return;
1312 mFooter.setVisibility(visible ? View.GONE : View.VISIBLE);
1313 mFooterDisambig.setVisibility(visible ? View.VISIBLE : View.GONE);
1316 // If showing list, then hide and save state of down arrow
1317 mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE);
1318 mArrowDown.setVisibility(View.INVISIBLE);
1320 // If hiding list, restore any down arrow state
1321 mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE);
1325 /** {@inheritDoc} */
1326 public void onClick(View view) {
1327 final boolean isActionView = (view instanceof CheckableImageView);
1328 final CheckableImageView actionView = isActionView ? (CheckableImageView)view : null;
1329 final Object tag = view.getTag();
1330 if (tag instanceof Action) {
1331 // Hide the resolution list, if present
1332 setResolveVisible(false, actionView);
1336 // Incoming tag is concrete intent, so try launching
1337 final Action action = (Action)tag;
1338 mContext.startActivity(action.getIntent());
1341 ContentValues values = new ContentValues(1);
1342 values.put(Data.IS_SUPER_PRIMARY, 1);
1343 final Uri dataUri = action.getDataUri();
1344 if (dataUri != null) {
1345 mContext.getContentResolver().update(dataUri, values, null, null);
1349 } catch (ActivityNotFoundException e) {
1350 Toast.makeText(mContext, R.string.quickcontact_missing_app, Toast.LENGTH_SHORT)
1353 } else if (tag instanceof ActionList) {
1354 // Incoming tag is a MIME-type, so show resolution list
1355 final ActionList children = (ActionList)tag;
1357 // Show resolution list and set adapter
1358 setResolveVisible(true, actionView);
1360 mResolveList.setOnItemClickListener(this);
1361 mResolveList.setAdapter(new BaseAdapter() {
1362 public int getCount() {
1363 return children.size();
1366 public Object getItem(int position) {
1367 return children.get(position);
1370 public long getItemId(int position) {
1374 public View getView(int position, View convertView, ViewGroup parent) {
1375 if (convertView == null) {
1376 convertView = mInflater.inflate(
1377 R.layout.quickcontact_resolve_item, parent, false);
1380 // Set action title based on summary value
1381 final Action action = (Action)getItem(position);
1382 final Drawable icon = mResolveCache.getIcon(action);
1384 TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
1385 TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
1387 text1.setText(action.getHeader());
1388 text2.setText(action.getBody());
1390 convertView.setTag(action);
1395 // Make sure we resize to make room for ListView
1396 onWindowAttributesChanged(mWindow.getAttributes());
1401 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
1402 mMakePrimary = isChecked;
1405 private void onBackPressed() {
1406 // Back key will first dismiss any expanded resolve list, otherwise
1407 // it will close the entire dialog.
1408 if (mFooterDisambig.getVisibility() == View.VISIBLE) {
1409 setResolveVisible(false, null);
1415 /** {@inheritDoc} */
1416 public boolean dispatchKeyEvent(KeyEvent event) {
1417 if (mWindow.superDispatchKeyEvent(event)) {
1420 return event.dispatch(this, mDecor != null
1421 ? mDecor.getKeyDispatcherState() : null, this);
1424 /** {@inheritDoc} */
1425 public boolean onKeyDown(int keyCode, KeyEvent event) {
1426 if (keyCode == KeyEvent.KEYCODE_BACK) {
1427 event.startTracking();
1434 /** {@inheritDoc} */
1435 public boolean onKeyUp(int keyCode, KeyEvent event) {
1436 if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
1437 && !event.isCanceled()) {
1445 /** {@inheritDoc} */
1446 public boolean onKeyLongPress(int keyCode, KeyEvent event) {
1450 /** {@inheritDoc} */
1451 public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
1455 /** {@inheritDoc} */
1456 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
1457 // TODO: make this window accessible
1462 * Detect if the given {@link MotionEvent} is outside the boundaries of this
1463 * window, which usually means we should dismiss.
1465 protected void detectEventOutside(MotionEvent event) {
1466 if (event.getAction() == MotionEvent.ACTION_DOWN) {
1467 // Only try detecting outside events on down-press
1468 mDecor.getHitRect(mRect);
1469 mRect.top = mRect.top + mDecor.getPaddingTop();
1470 mRect.bottom = mRect.bottom - mDecor.getPaddingBottom();
1471 final int x = (int)event.getX();
1472 final int y = (int)event.getY();
1473 if (!mRect.contains(x, y)) {
1474 event.setAction(MotionEvent.ACTION_OUTSIDE);
1479 /** {@inheritDoc} */
1480 public boolean dispatchTouchEvent(MotionEvent event) {
1481 detectEventOutside(event);
1482 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
1486 return mWindow.superDispatchTouchEvent(event);
1490 /** {@inheritDoc} */
1491 public boolean dispatchTrackballEvent(MotionEvent event) {
1492 return mWindow.superDispatchTrackballEvent(event);
1495 /** {@inheritDoc} */
1496 public void onContentChanged() {
1499 /** {@inheritDoc} */
1500 public boolean onCreatePanelMenu(int featureId, Menu menu) {
1504 /** {@inheritDoc} */
1505 public View onCreatePanelView(int featureId) {
1509 /** {@inheritDoc} */
1510 public boolean onMenuItemSelected(int featureId, MenuItem item) {
1514 /** {@inheritDoc} */
1515 public boolean onMenuOpened(int featureId, Menu menu) {
1519 /** {@inheritDoc} */
1520 public void onPanelClosed(int featureId, Menu menu) {
1523 /** {@inheritDoc} */
1524 public boolean onPreparePanel(int featureId, View view, Menu menu) {
1528 /** {@inheritDoc} */
1529 public boolean onSearchRequested() {
1533 /** {@inheritDoc} */
1534 public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
1535 if (mDecor != null) {
1536 mWindowManager.updateViewLayout(mDecor, attrs);
1540 /** {@inheritDoc} */
1541 public void onWindowFocusChanged(boolean hasFocus) {
1544 /** {@inheritDoc} */
1545 public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
1549 /** {@inheritDoc} */
1550 public void onAttachedToWindow() {
1554 /** {@inheritDoc} */
1555 public void onDetachedFromWindow() {
1559 private interface DataQuery {
1560 final String[] PROJECTION = new String[] {
1563 RawContacts.ACCOUNT_TYPE,
1565 Contacts.DISPLAY_NAME,
1566 Contacts.CONTACT_PRESENCE,
1569 Data.STATUS_RES_PACKAGE,
1572 Data.STATUS_TIMESTAMP,
1578 Data.IS_SUPER_PRIMARY,
1579 Data.RAW_CONTACT_ID,
1581 Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
1582 Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11,
1583 Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
1588 final int ACCOUNT_TYPE = 1;
1589 final int STARRED = 2;
1590 final int DISPLAY_NAME = 3;
1591 final int CONTACT_PRESENCE = 4;
1593 final int STATUS = 5;
1594 final int STATUS_RES_PACKAGE = 6;
1595 final int STATUS_ICON = 7;
1596 final int STATUS_LABEL = 8;
1597 final int STATUS_TIMESTAMP = 9;
1598 final int PRESENCE = 10;
1600 final int RES_PACKAGE = 11;
1601 final int MIMETYPE = 12;
1602 final int IS_PRIMARY = 13;
1603 final int IS_SUPER_PRIMARY = 14;