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