QuickContactActivity.java revision 90921b3bfd9b3492a19a5fbbf6e0309b97a32425
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.contacts.quickcontact;
18
19import com.android.contacts.Collapser;
20import com.android.contacts.ContactPhotoManager;
21import com.android.contacts.R;
22import com.android.contacts.model.AccountTypeManager;
23import com.android.contacts.model.DataKind;
24import com.android.contacts.util.DataStatus;
25import com.android.contacts.util.NotifyingAsyncQueryHandler;
26import com.android.contacts.util.NotifyingAsyncQueryHandler.AsyncQueryListener;
27import com.google.common.base.Preconditions;
28import com.google.common.collect.Lists;
29
30import android.app.Activity;
31import android.app.Fragment;
32import android.app.FragmentManager;
33import android.content.ActivityNotFoundException;
34import android.content.ContentUris;
35import android.content.Context;
36import android.content.Intent;
37import android.content.pm.PackageManager;
38import android.content.res.AssetFileDescriptor;
39import android.database.Cursor;
40import android.graphics.Bitmap;
41import android.graphics.BitmapFactory;
42import android.graphics.Rect;
43import android.graphics.drawable.Drawable;
44import android.net.Uri;
45import android.os.AsyncTask;
46import android.os.Bundle;
47import android.os.Handler;
48import android.provider.ContactsContract.CommonDataKinds.Email;
49import android.provider.ContactsContract.CommonDataKinds.Im;
50import android.provider.ContactsContract.CommonDataKinds.Phone;
51import android.provider.ContactsContract.CommonDataKinds.Photo;
52import android.provider.ContactsContract.CommonDataKinds.SipAddress;
53import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
54import android.provider.ContactsContract.CommonDataKinds.Website;
55import android.provider.ContactsContract.Contacts;
56import android.provider.ContactsContract.Data;
57import android.provider.ContactsContract.DisplayPhoto;
58import android.provider.ContactsContract.QuickContact;
59import android.provider.ContactsContract.RawContacts;
60import android.support.v13.app.FragmentPagerAdapter;
61import android.support.v4.view.ViewPager;
62import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
63import android.text.TextUtils;
64import android.util.Log;
65import android.view.MotionEvent;
66import android.view.View;
67import android.view.View.OnClickListener;
68import android.view.ViewGroup;
69import android.view.WindowManager;
70import android.widget.HorizontalScrollView;
71import android.widget.ImageButton;
72import android.widget.ImageView;
73import android.widget.RelativeLayout;
74import android.widget.TextView;
75import android.widget.Toast;
76
77import java.io.IOException;
78import java.util.HashMap;
79import java.util.HashSet;
80import java.util.List;
81import java.util.Set;
82
83// TODO: Save selected tab index during rotation
84
85/**
86 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
87 * data asynchronously, and then shows a popup with details centered around
88 * {@link Intent#getSourceBounds()}.
89 */
90public class QuickContactActivity extends Activity {
91    private static final String TAG = "QuickContact";
92
93    private static final boolean TRACE_LAUNCH = false;
94    private static final String TRACE_TAG = "quickcontact";
95
96    @SuppressWarnings("deprecation")
97    private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
98
99    private NotifyingAsyncQueryHandler mHandler;
100
101    private Uri mLookupUri;
102    private String[] mExcludeMimes;
103    private List<String> mSortedActionMimeTypes = Lists.newArrayList();
104
105    private boolean mHasFinishedAnimatingIn = false;
106    private boolean mHasStartedAnimatingOut = false;
107
108    private FloatingChildLayout mFloatingLayout;
109
110    private View mPhotoContainer;
111    private ViewGroup mTrack;
112    private HorizontalScrollView mTrackScroller;
113    private View mSelectedTabRectangle;
114    private View mLineAfterTrack;
115
116    private ImageButton mOpenDetailsButton;
117    private ImageButton mOpenDetailsPushLayerButton;
118    private ViewPager mListPager;
119
120    /**
121     * Keeps the default action per mimetype. Empty if no default actions are set
122     */
123    private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>();
124
125    /**
126     * Set of {@link Action} that are associated with the aggregate currently
127     * displayed by this dialog, represented as a map from {@link String}
128     * MIME-type to a list of {@link Action}.
129     */
130    private ActionMultiMap mActions = new ActionMultiMap();
131
132    /**
133     * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types.
134     *
135     * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
136     * in the order specified here.</p>
137     *
138     * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order
139     * specified here.</p>
140     *
141     * <p>The rest go between them, in the order in the array.</p>
142     */
143    private static final List<String> LEADING_MIMETYPES = Lists.newArrayList(
144            Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE);
145
146    /** See {@link #LEADING_MIMETYPES}. */
147    private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
148            StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
149
150    /** Id for the background handler that loads the data */
151    private static final int HANDLER_ID_DATA = 1;
152
153    @Override
154    protected void onCreate(Bundle icicle) {
155        super.onCreate(icicle);
156
157        // Show QuickContact in front of soft input
158        getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
159                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
160
161        setContentView(R.layout.quickcontact_activity);
162
163        mFloatingLayout = (FloatingChildLayout) findViewById(R.id.floating_layout);
164        mTrack = (ViewGroup) findViewById(R.id.track);
165        mTrackScroller = (HorizontalScrollView) findViewById(R.id.track_scroller);
166        mOpenDetailsButton = (ImageButton) findViewById(R.id.open_details_button);
167        mOpenDetailsPushLayerButton = (ImageButton) findViewById(R.id.open_details_push_layer);
168        mListPager = (ViewPager) findViewById(R.id.item_list_pager);
169        mSelectedTabRectangle = findViewById(R.id.selected_tab_rectangle);
170        mLineAfterTrack = findViewById(R.id.line_after_track);
171
172        mFloatingLayout.setOnOutsideTouchListener(new View.OnTouchListener() {
173            @Override
174            public boolean onTouch(View v, MotionEvent event) {
175                return handleOutsideTouch();
176            }
177        });
178
179        final OnClickListener openDetailsClickHandler = new OnClickListener() {
180            @Override
181            public void onClick(View v) {
182                final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
183                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
184                startActivity(intent);
185                hide(false);
186            }
187        };
188        mOpenDetailsButton.setOnClickListener(openDetailsClickHandler);
189        mOpenDetailsPushLayerButton.setOnClickListener(openDetailsClickHandler);
190        mListPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
191        mListPager.setOnPageChangeListener(new PageChangeListener());
192
193        mHandler = new NotifyingAsyncQueryHandler(this, mQueryListener);
194
195        show();
196    }
197
198    private void show() {
199
200        if (TRACE_LAUNCH) {
201            android.os.Debug.startMethodTracing(TRACE_TAG);
202        }
203
204        final Intent intent = getIntent();
205
206        Uri lookupUri = intent.getData();
207
208        // Check to see whether it comes from the old version.
209        if (LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
210            final long rawContactId = ContentUris.parseId(lookupUri);
211            lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
212                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
213        }
214
215        mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
216
217        // Read requested parameters for displaying
218        final Rect targetScreen = intent.getSourceBounds();
219        Preconditions.checkNotNull(targetScreen, "missing targetScreen");
220        mFloatingLayout.setChildTargetScreen(targetScreen);
221
222        mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
223
224        // find and prepare correct header view
225        mPhotoContainer = findViewById(R.id.photo_container);
226        setHeaderNameText(R.id.name, R.string.missing_name);
227
228        // Start background query for data, but only select photo rows when they
229        // directly match the super-primary PHOTO_ID.
230        final Uri dataUri = Uri.withAppendedPath(lookupUri, Contacts.Data.CONTENT_DIRECTORY);
231        mHandler.cancelOperation(HANDLER_ID_DATA);
232
233        // Select all data items of the contact (except for photos, where we only select the display
234        // photo)
235        mHandler.startQuery(HANDLER_ID_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
236                + "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
237                + ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
238    }
239
240    private boolean handleOutsideTouch() {
241        if (!mHasFinishedAnimatingIn) return false;
242        if (mHasStartedAnimatingOut) return false;
243
244        mHasStartedAnimatingOut = true;
245        hide(true);
246        return true;
247    }
248
249    private void hide(boolean withAnimation) {
250        // cancel any pending queries
251        mHandler.cancelOperation(HANDLER_ID_DATA);
252
253        if (withAnimation) {
254            mFloatingLayout.hideChild(new Runnable() {
255                @Override
256                public void run() {
257                    finish();
258                }
259            });
260        } else {
261            mFloatingLayout.hideChild(null);
262            finish();
263        }
264    }
265
266    @Override
267    public void onBackPressed() {
268        hide(true);
269    }
270
271    private final AsyncQueryListener mQueryListener = new AsyncQueryListener() {
272        @Override
273        public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
274            try {
275                if (isFinishing()) {
276                    hide(false);
277                    return;
278                } else if (cursor == null || cursor.getCount() == 0) {
279                    Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
280                            Toast.LENGTH_LONG).show();
281                    hide(false);
282                    return;
283                }
284
285                bindData(cursor);
286
287                if (TRACE_LAUNCH) {
288                    android.os.Debug.stopMethodTracing();
289                }
290
291                // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
292                // that the layout passes are completed
293                mHandler.post(new Runnable() {
294                    @Override
295                    public void run() {
296                        mFloatingLayout.showChild(new Runnable() {
297                            @Override
298                            public void run() {
299                                mHasFinishedAnimatingIn = true;
300                            }
301                        });
302                    }
303                });
304            } finally {
305                if (cursor != null) {
306                    cursor.close();
307                }
308            }
309        }
310    };
311
312    /** Assign this string to the view if it is not empty. */
313    private void setHeaderNameText(int id, int resId) {
314        setHeaderNameText(id, getText(resId));
315    }
316
317    /** Assign this string to the view if it is not empty. */
318    private void setHeaderNameText(int id, CharSequence value) {
319        final View view = mPhotoContainer.findViewById(id);
320        if (view instanceof TextView) {
321            if (!TextUtils.isEmpty(value)) {
322                ((TextView)view).setText(value);
323            }
324        }
325    }
326
327    /**
328     * Assign this string to the view (if found in {@link #mPhotoContainer}), or hiding this view
329     * if there is no string.
330     */
331    private void setHeaderText(int id, int resId) {
332        setHeaderText(id, getText(resId));
333    }
334
335    /**
336     * Assign this string to the view (if found in {@link #mPhotoContainer}), or hiding this view
337     * if there is no string.
338     */
339    private void setHeaderText(int id, CharSequence value) {
340        final View view = mPhotoContainer.findViewById(id);
341        if (view instanceof TextView) {
342            ((TextView)view).setText(value);
343            view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE);
344        }
345    }
346
347    /** Assign this image to the view, if found in {@link #mPhotoContainer}. */
348    private void setHeaderImage(int id, Drawable drawable) {
349        final View view = mPhotoContainer.findViewById(id);
350        if (view instanceof ImageView) {
351            ((ImageView)view).setImageDrawable(drawable);
352            view.setVisibility(drawable == null ? View.GONE : View.VISIBLE);
353        }
354    }
355
356    /**
357     * Check if the given MIME-type appears in the list of excluded MIME-types
358     * that the most-recent caller requested.
359     */
360    private boolean isMimeExcluded(String mimeType) {
361        if (mExcludeMimes == null) return false;
362        for (String excludedMime : mExcludeMimes) {
363            if (TextUtils.equals(excludedMime, mimeType)) {
364                return true;
365            }
366        }
367        return false;
368    }
369
370    /**
371     * Handle the result from the {@link #TOKEN_DATA} query.
372     */
373    private void bindData(Cursor cursor) {
374        final ResolveCache cache = ResolveCache.getInstance(this);
375        final Context context = this;
376
377        mOpenDetailsButton.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? View.GONE
378                : View.VISIBLE);
379
380        mDefaultsMap.clear();
381
382        final DataStatus status = new DataStatus();
383        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
384                context.getApplicationContext());
385        final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
386
387        Bitmap photoBitmap = null;
388        while (cursor.moveToNext()) {
389            // Handle any social status updates from this row
390            status.possibleUpdate(cursor);
391
392            final String mimeType = cursor.getString(DataQuery.MIMETYPE);
393
394            // Skip this data item if MIME-type excluded
395            if (isMimeExcluded(mimeType)) continue;
396
397            final long dataId = cursor.getLong(DataQuery._ID);
398            final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
399            final String dataSet = cursor.getString(DataQuery.DATA_SET);
400            final boolean isPrimary = cursor.getInt(DataQuery.IS_PRIMARY) != 0;
401            final boolean isSuperPrimary = cursor.getInt(DataQuery.IS_SUPER_PRIMARY) != 0;
402
403            // Handle photos included as data row
404            if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
405                final int displayPhotoColumnIndex = cursor.getColumnIndex(Photo.PHOTO_FILE_ID);
406                final boolean hasDisplayPhoto = !cursor.isNull(displayPhotoColumnIndex);
407                if (hasDisplayPhoto) {
408                    final long displayPhotoId = cursor.getLong(displayPhotoColumnIndex);
409                    final Uri displayPhotoUri = ContentUris.withAppendedId(
410                            DisplayPhoto.CONTENT_URI, displayPhotoId);
411                    // Fetch and JPEG uncompress on the background thread
412                    new AsyncTask<Void, Void, Bitmap>() {
413                        @Override
414                        protected Bitmap doInBackground(Void... params) {
415                            try {
416                                AssetFileDescriptor fd = getContentResolver()
417                                        .openAssetFileDescriptor(displayPhotoUri, "r");
418                                return BitmapFactory.decodeStream(fd.createInputStream());
419                            } catch (IOException e) {
420                                Log.e(TAG, "Error getting display photo. Ignoring, as we already " +
421                                        "have the thumbnail", e);
422                                return null;
423                            }
424                        }
425
426                        @Override
427                        protected void onPostExecute(Bitmap result) {
428                            if (result == null) return;
429                            photoView.setImageBitmap(result);
430                        }
431                    }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
432                }
433                final int photoColumnIndex = cursor.getColumnIndex(Photo.PHOTO);
434                final byte[] photoBlob = cursor.getBlob(photoColumnIndex);
435                if (photoBlob != null) {
436                    photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
437                }
438                continue;
439            }
440
441            final DataKind kind = accountTypes.getKindOrFallback(accountType, dataSet, mimeType);
442
443            if (kind != null) {
444                // Build an action for this data entry, find a mapping to a UI
445                // element, build its summary from the cursor, and collect it
446                // along with all others of this MIME-type.
447                final Action action = new DataAction(context, mimeType, kind, dataId, cursor);
448                final boolean wasAdded = considerAdd(action, cache);
449                if (wasAdded) {
450                    // Remember the default
451                    if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
452                        mDefaultsMap.put(mimeType, action);
453                    }
454                }
455            }
456
457            // Handle Email rows with presence data as Im entry
458            final boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE);
459            if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
460                final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet,
461                        Im.CONTENT_ITEM_TYPE);
462                if (imKind != null) {
463                    final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE, imKind,
464                            dataId, cursor);
465                    considerAdd(action, cache);
466                }
467            }
468        }
469
470        // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
471        for (List<Action> actionChildren : mActions.values()) {
472            Collapser.collapseList(actionChildren);
473        }
474
475        if (cursor.moveToLast()) {
476            // Read contact name from last data row
477            final String name = cursor.getString(DataQuery.DISPLAY_NAME);
478            setHeaderNameText(R.id.name, name);
479        }
480
481        if (photoView != null) {
482            // Place photo when discovered in data, otherwise show generic avatar
483            if (photoBitmap != null) {
484                photoView.setImageBitmap(photoBitmap);
485            } else {
486                photoView.setImageResource(ContactPhotoManager.getDefaultAvatarResId(true, false));
487            }
488        }
489
490        // All the mime-types to add.
491        final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
492        mSortedActionMimeTypes.clear();
493        // First, add LEADING_MIMETYPES, which are most common.
494        for (String mimeType : LEADING_MIMETYPES) {
495            if (containedTypes.contains(mimeType)) {
496                mSortedActionMimeTypes.add(mimeType);
497                containedTypes.remove(mimeType);
498            }
499        }
500
501        // Add all the remaining ones that are not TRAILING
502        for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) {
503            if (!TRAILING_MIMETYPES.contains(mimeType)) {
504                mSortedActionMimeTypes.add(mimeType);
505                containedTypes.remove(mimeType);
506            }
507        }
508
509        // Then, add TRAILING_MIMETYPES, which are least common.
510        for (String mimeType : TRAILING_MIMETYPES) {
511            if (containedTypes.contains(mimeType)) {
512                containedTypes.remove(mimeType);
513                mSortedActionMimeTypes.add(mimeType);
514            }
515        }
516
517        // Add buttons for each mimetype
518        for (String mimeType : mSortedActionMimeTypes) {
519            final View actionView = inflateAction(mimeType, cache, mTrack);
520            mTrack.addView(actionView);
521        }
522
523        final boolean hasData = !mSortedActionMimeTypes.isEmpty();
524        mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE);
525        mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE);
526        mLineAfterTrack.setVisibility(hasData ? View.VISIBLE : View.GONE);
527        mListPager.setVisibility(hasData ? View.VISIBLE : View.GONE);
528    }
529
530    /**
531     * Consider adding the given {@link Action}, which will only happen if
532     * {@link PackageManager} finds an application to handle
533     * {@link Action#getIntent()}.
534     * @return true if action has been added
535     */
536    private boolean considerAdd(Action action, ResolveCache resolveCache) {
537        if (resolveCache.hasResolve(action)) {
538            mActions.put(action.getMimeType(), action);
539            return true;
540        }
541        return false;
542    }
543
544    /**
545     * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
546     * Will use the icon provided by the {@link DataKind}.
547     */
548    private View inflateAction(String mimeType, ResolveCache resolveCache, ViewGroup root) {
549        final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate(
550                R.layout.quickcontact_track_button, root, false);
551
552        List<Action> children = mActions.get(mimeType);
553        typeView.setTag(mimeType);
554        final Action firstInfo = children.get(0);
555
556        // Set icon and listen for clicks
557        final CharSequence descrip = resolveCache.getDescription(firstInfo);
558        final Drawable icon = resolveCache.getIcon(firstInfo);
559        typeView.setChecked(false);
560        typeView.setContentDescription(descrip);
561        typeView.setImageDrawable(icon);
562        typeView.setOnClickListener(mTypeViewClickListener);
563
564        return typeView;
565    }
566
567    private CheckableImageView getActionViewAt(int position) {
568        return (CheckableImageView) mTrack.getChildAt(position);
569    }
570
571    @Override
572    public void onAttachFragment(Fragment fragment) {
573        final QuickContactListFragment listFragment = (QuickContactListFragment) fragment;
574        listFragment.setListener(mListFragmentListener);
575    }
576
577    /** A type (e.g. Call/Addresses was clicked) */
578    private final OnClickListener mTypeViewClickListener = new OnClickListener() {
579        @Override
580        public void onClick(View view) {
581            final CheckableImageView actionView = (CheckableImageView)view;
582            final String mimeType = (String) actionView.getTag();
583            int index = mSortedActionMimeTypes.indexOf(mimeType);
584            mListPager.setCurrentItem(index, true);
585        }
586    };
587
588    private class ViewPagerAdapter extends FragmentPagerAdapter {
589        public ViewPagerAdapter(FragmentManager fragmentManager) {
590            super(fragmentManager);
591        }
592
593        @Override
594        public Fragment getItem(int position) {
595            QuickContactListFragment fragment = new QuickContactListFragment();
596            final String mimeType = mSortedActionMimeTypes.get(position);
597            final List<Action> actions = mActions.get(mimeType);
598            fragment.setActions(actions);
599            return fragment;
600        }
601
602        @Override
603        public int getCount() {
604            return mSortedActionMimeTypes.size();
605        }
606    }
607
608    private class PageChangeListener extends SimpleOnPageChangeListener {
609        @Override
610        public void onPageSelected(int position) {
611            final CheckableImageView actionView = getActionViewAt(position);
612            mTrackScroller.requestChildRectangleOnScreen(actionView,
613                    new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false);
614        }
615
616        @Override
617        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
618            final RelativeLayout.LayoutParams layoutParams =
619                    (RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams();
620            final int width = mSelectedTabRectangle.getWidth();
621            layoutParams.leftMargin = (int) ((position + positionOffset) * width);
622            mSelectedTabRectangle.setLayoutParams(layoutParams);
623        }
624    }
625
626    private final QuickContactListFragment.Listener mListFragmentListener =
627            new QuickContactListFragment.Listener() {
628        @Override
629        public void onOutsideClick() {
630            // If there is no background, we want to dismiss, because to the user it seems
631            // like he had touched outside. If the ViewPager is solid however, those taps
632            // must be ignored
633            final boolean isTransparent = mListPager.getBackground() == null;
634            if (isTransparent) handleOutsideTouch();
635        }
636
637        @Override
638        public void onItemClicked(final Action action, final boolean alternate) {
639            final Runnable startAppRunnable = new Runnable() {
640                @Override
641                public void run() {
642                    try {
643                        startActivity(alternate ? action.getAlternateIntent() : action.getIntent());
644                    } catch (ActivityNotFoundException e) {
645                        Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app,
646                                Toast.LENGTH_SHORT).show();
647                    }
648
649                    hide(false);
650                }
651            };
652            // Defer the action to make the window properly repaint
653            new Handler().post(startAppRunnable);
654        }
655    };
656
657    private interface DataQuery {
658        final String[] PROJECTION = new String[] {
659                Data._ID,
660
661                RawContacts.ACCOUNT_TYPE,
662                RawContacts.DATA_SET,
663                Contacts.STARRED,
664                Contacts.DISPLAY_NAME,
665
666                Data.STATUS,
667                Data.STATUS_RES_PACKAGE,
668                Data.STATUS_ICON,
669                Data.STATUS_LABEL,
670                Data.STATUS_TIMESTAMP,
671                Data.PRESENCE,
672                Data.CHAT_CAPABILITY,
673
674                Data.RES_PACKAGE,
675                Data.MIMETYPE,
676                Data.IS_PRIMARY,
677                Data.IS_SUPER_PRIMARY,
678                Data.RAW_CONTACT_ID,
679
680                Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
681                Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11,
682                Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
683        };
684
685        final int _ID = 0;
686
687        final int ACCOUNT_TYPE = 1;
688        final int DATA_SET = 2;
689        final int STARRED = 3;
690        final int DISPLAY_NAME = 4;
691
692        final int STATUS = 5;
693        final int STATUS_RES_PACKAGE = 6;
694        final int STATUS_ICON = 7;
695        final int STATUS_LABEL = 8;
696        final int STATUS_TIMESTAMP = 9;
697        final int PRESENCE = 10;
698        final int CHAT_CAPABILITY = 11;
699
700        final int RES_PACKAGE = 12;
701        final int MIMETYPE = 13;
702        final int IS_PRIMARY = 14;
703        final int IS_SUPER_PRIMARY = 15;
704    }
705}
706