QuickContactActivity.java revision d9662a844162bb99df65e7976dfa335e64ca5440
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.ContactLoader;
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.ImageViewDrawableSetter;
26import com.android.contacts.util.SchedulingUtils;
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.app.LoaderManager.LoaderCallbacks;
34import android.content.ActivityNotFoundException;
35import android.content.ContentUris;
36import android.content.ContentValues;
37import android.content.Context;
38import android.content.Entity;
39import android.content.Entity.NamedContentValues;
40import android.content.Intent;
41import android.content.Loader;
42import android.content.pm.PackageManager;
43import android.graphics.Rect;
44import android.graphics.drawable.Drawable;
45import android.net.Uri;
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.SipAddress;
52import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
53import android.provider.ContactsContract.CommonDataKinds.Website;
54import android.provider.ContactsContract.Contacts;
55import android.provider.ContactsContract.Data;
56import android.provider.ContactsContract.QuickContact;
57import android.provider.ContactsContract.RawContacts;
58import android.support.v13.app.FragmentPagerAdapter;
59import android.support.v4.view.ViewPager;
60import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
61import android.text.TextUtils;
62import android.util.Log;
63import android.view.MotionEvent;
64import android.view.View;
65import android.view.View.OnClickListener;
66import android.view.ViewGroup;
67import android.view.WindowManager;
68import android.widget.HorizontalScrollView;
69import android.widget.ImageButton;
70import android.widget.ImageView;
71import android.widget.RelativeLayout;
72import android.widget.TextView;
73import android.widget.Toast;
74
75import java.util.HashMap;
76import java.util.HashSet;
77import java.util.List;
78import java.util.Set;
79
80// TODO: Save selected tab index during rotation
81
82/**
83 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
84 * data asynchronously, and then shows a popup with details centered around
85 * {@link Intent#getSourceBounds()}.
86 */
87public class QuickContactActivity extends Activity {
88    private static final String TAG = "QuickContact";
89
90    private static final boolean TRACE_LAUNCH = false;
91    private static final String TRACE_TAG = "quickcontact";
92    private static final int POST_DRAW_WAIT_DURATION = 60;
93
94    @SuppressWarnings("deprecation")
95    private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
96
97    private Uri mLookupUri;
98    private String[] mExcludeMimes;
99    private List<String> mSortedActionMimeTypes = Lists.newArrayList();
100
101    private boolean mHasFinishedAnimatingIn = false;
102    private boolean mHasStartedAnimatingOut = false;
103
104    private FloatingChildLayout mFloatingLayout;
105
106    private View mPhotoContainer;
107    private ViewGroup mTrack;
108    private HorizontalScrollView mTrackScroller;
109    private View mSelectedTabRectangle;
110    private View mLineAfterTrack;
111
112    private ImageButton mOpenDetailsButton;
113    private ImageButton mOpenDetailsPushLayerButton;
114    private ViewPager mListPager;
115
116    private ContactLoader mContactLoader;
117
118    private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
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 loader */
151    private static final int LOADER_ID = 0;
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                mContactLoader.cacheResult();
184                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
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        show();
195    }
196
197    private void show() {
198
199        if (TRACE_LAUNCH) {
200            android.os.Debug.startMethodTracing(TRACE_TAG);
201        }
202
203        final Intent intent = getIntent();
204
205        Uri lookupUri = intent.getData();
206
207        // Check to see whether it comes from the old version.
208        if (LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
209            final long rawContactId = ContentUris.parseId(lookupUri);
210            lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
211                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
212        }
213
214        mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
215
216        // Read requested parameters for displaying
217        final Rect targetScreen = intent.getSourceBounds();
218        Preconditions.checkNotNull(targetScreen, "missing targetScreen");
219        mFloatingLayout.setChildTargetScreen(targetScreen);
220
221        mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
222
223        // find and prepare correct header view
224        mPhotoContainer = findViewById(R.id.photo_container);
225        setHeaderNameText(R.id.name, R.string.missing_name);
226
227        mContactLoader = (ContactLoader) getLoaderManager().initLoader(
228                LOADER_ID, null, mLoaderCallbacks);
229    }
230
231    private boolean handleOutsideTouch() {
232        if (!mHasFinishedAnimatingIn) return false;
233        if (mHasStartedAnimatingOut) return false;
234
235        mHasStartedAnimatingOut = true;
236        hide(true);
237        return true;
238    }
239
240    private void hide(boolean withAnimation) {
241        // cancel any pending queries
242        getLoaderManager().destroyLoader(LOADER_ID);
243
244        if (withAnimation) {
245            mFloatingLayout.hideChild(new Runnable() {
246                @Override
247                public void run() {
248                    // Wait until the final animation frame has been drawn, otherwise
249                    // there is jank as the framework transitions to the next Activity.
250                    SchedulingUtils.doAfterDraw(mFloatingLayout, new Runnable() {
251                        @Override
252                        public void run() {
253                            // Unfortunately, we need to also use postDelayed() to wait a moment
254                            // for the frame to be drawn, else the framework's activity-transition
255                            // animation will kick in before the final frame is available to it.
256                            // This seems unavoidable.  The problem isn't merely that there is no
257                            // post-draw listener API; if that were so, it would be sufficient to
258                            // call post() instead of postDelayed().
259                            new Handler().postDelayed(new Runnable() {
260                                @Override
261                                public void run() {
262                                    finish();
263                                }
264                            }, POST_DRAW_WAIT_DURATION);
265                        }
266                    });
267                }
268            });
269        } else {
270            mFloatingLayout.hideChild(null);
271            finish();
272        }
273    }
274
275    @Override
276    public void onBackPressed() {
277        hide(true);
278    }
279
280    /** Assign this string to the view if it is not empty. */
281    private void setHeaderNameText(int id, int resId) {
282        setHeaderNameText(id, getText(resId));
283    }
284
285    /** Assign this string to the view if it is not empty. */
286    private void setHeaderNameText(int id, CharSequence value) {
287        final View view = mPhotoContainer.findViewById(id);
288        if (view instanceof TextView) {
289            if (!TextUtils.isEmpty(value)) {
290                ((TextView)view).setText(value);
291            }
292        }
293    }
294
295    /**
296     * Check if the given MIME-type appears in the list of excluded MIME-types
297     * that the most-recent caller requested.
298     */
299    private boolean isMimeExcluded(String mimeType) {
300        if (mExcludeMimes == null) return false;
301        for (String excludedMime : mExcludeMimes) {
302            if (TextUtils.equals(excludedMime, mimeType)) {
303                return true;
304            }
305        }
306        return false;
307    }
308
309    /**
310     * Handle the result from the ContactLoader
311     */
312    private void bindData(ContactLoader.Result data) {
313        final ResolveCache cache = ResolveCache.getInstance(this);
314        final Context context = this;
315
316        mOpenDetailsButton.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? View.GONE
317                : View.VISIBLE);
318
319        mDefaultsMap.clear();
320
321        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
322                context.getApplicationContext());
323        final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
324        mPhotoSetter.setupContactPhoto(data, photoView);
325
326        for (Entity entity : data.getEntities()) {
327            final ContentValues entityValues = entity.getEntityValues();
328            final String accountType = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
329            final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
330            for (NamedContentValues subValue : entity.getSubValues()) {
331                final ContentValues entryValues = subValue.values;
332                final String mimeType = entryValues.getAsString(Data.MIMETYPE);
333
334                // Skip this data item if MIME-type excluded
335                if (isMimeExcluded(mimeType)) continue;
336
337                final long dataId = entryValues.getAsLong(Data._ID);
338                final Integer primary = entryValues.getAsInteger(Data.IS_PRIMARY);
339                final boolean isPrimary = primary != null && primary != 0;
340                final Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY);
341                final boolean isSuperPrimary = superPrimary != null && superPrimary != 0;
342
343                final DataKind kind =
344                        accountTypes.getKindOrFallback(accountType, dataSet, mimeType);
345
346                if (kind != null) {
347                    // Build an action for this data entry, find a mapping to a UI
348                    // element, build its summary from the cursor, and collect it
349                    // along with all others of this MIME-type.
350                    final Action action = new DataAction(context, mimeType, kind, dataId,
351                            entryValues);
352                    final boolean wasAdded = considerAdd(action, cache, isSuperPrimary);
353                    if (wasAdded) {
354                        // Remember the default
355                        if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
356                            mDefaultsMap.put(mimeType, action);
357                        }
358                    }
359                }
360
361                // Handle Email rows with presence data as Im entry
362                final DataStatus status = data.getStatuses().get(dataId);
363                if (status != null && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
364                    final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet,
365                            Im.CONTENT_ITEM_TYPE);
366                    if (imKind != null) {
367                        final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE,
368                                imKind, dataId, entryValues);
369                        action.setPresence(status.getPresence());
370                        considerAdd(action, cache, isSuperPrimary);
371                    }
372                }
373            }
374        }
375
376        // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
377        for (List<Action> actionChildren : mActions.values()) {
378            Collapser.collapseList(actionChildren);
379        }
380
381        setHeaderNameText(R.id.name, data.getDisplayName());
382
383        // All the mime-types to add.
384        final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
385        mSortedActionMimeTypes.clear();
386        // First, add LEADING_MIMETYPES, which are most common.
387        for (String mimeType : LEADING_MIMETYPES) {
388            if (containedTypes.contains(mimeType)) {
389                mSortedActionMimeTypes.add(mimeType);
390                containedTypes.remove(mimeType);
391            }
392        }
393
394        // Add all the remaining ones that are not TRAILING
395        for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) {
396            if (!TRAILING_MIMETYPES.contains(mimeType)) {
397                mSortedActionMimeTypes.add(mimeType);
398                containedTypes.remove(mimeType);
399            }
400        }
401
402        // Then, add TRAILING_MIMETYPES, which are least common.
403        for (String mimeType : TRAILING_MIMETYPES) {
404            if (containedTypes.contains(mimeType)) {
405                containedTypes.remove(mimeType);
406                mSortedActionMimeTypes.add(mimeType);
407            }
408        }
409
410        // Add buttons for each mimetype
411        mTrack.removeAllViews();
412        for (String mimeType : mSortedActionMimeTypes) {
413            final View actionView = inflateAction(mimeType, cache, mTrack);
414            mTrack.addView(actionView);
415        }
416
417        final boolean hasData = !mSortedActionMimeTypes.isEmpty();
418        mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE);
419        mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE);
420        mLineAfterTrack.setVisibility(hasData ? View.VISIBLE : View.GONE);
421        mListPager.setVisibility(hasData ? View.VISIBLE : View.GONE);
422    }
423
424    /**
425     * Consider adding the given {@link Action}, which will only happen if
426     * {@link PackageManager} finds an application to handle
427     * {@link Action#getIntent()}.
428     * @param action the action to handle
429     * @param resolveCache cache of applications that can handle actions
430     * @param front indicates whether to add the action to the front of the list
431     * @return true if action has been added
432     */
433    private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) {
434        if (resolveCache.hasResolve(action)) {
435            mActions.put(action.getMimeType(), action, front);
436            return true;
437        }
438        return false;
439    }
440
441    /**
442     * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
443     * Will use the icon provided by the {@link DataKind}.
444     */
445    private View inflateAction(String mimeType, ResolveCache resolveCache, ViewGroup root) {
446        final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate(
447                R.layout.quickcontact_track_button, root, false);
448
449        List<Action> children = mActions.get(mimeType);
450        typeView.setTag(mimeType);
451        final Action firstInfo = children.get(0);
452
453        // Set icon and listen for clicks
454        final CharSequence descrip = resolveCache.getDescription(firstInfo);
455        final Drawable icon = resolveCache.getIcon(firstInfo);
456        typeView.setChecked(false);
457        typeView.setContentDescription(descrip);
458        typeView.setImageDrawable(icon);
459        typeView.setOnClickListener(mTypeViewClickListener);
460
461        return typeView;
462    }
463
464    private CheckableImageView getActionViewAt(int position) {
465        return (CheckableImageView) mTrack.getChildAt(position);
466    }
467
468    @Override
469    public void onAttachFragment(Fragment fragment) {
470        final QuickContactListFragment listFragment = (QuickContactListFragment) fragment;
471        listFragment.setListener(mListFragmentListener);
472    }
473
474    private LoaderCallbacks<ContactLoader.Result> mLoaderCallbacks =
475            new LoaderCallbacks<ContactLoader.Result>() {
476        @Override
477        public void onLoaderReset(Loader<ContactLoader.Result> loader) {
478        }
479
480        @Override
481        public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
482            if (isFinishing()) {
483                hide(false);
484                return;
485            }
486            if (data.isError()) {
487                // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
488                // should log the actual exception.
489                throw new IllegalStateException("Failed to load contact", data.getException());
490            }
491            if (data.isNotFound()) {
492                Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
493                Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
494                        Toast.LENGTH_LONG).show();
495                hide(false);
496                return;
497            }
498
499            bindData(data);
500
501            if (TRACE_LAUNCH) {
502                android.os.Debug.stopMethodTracing();
503            }
504
505            // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
506            // that the layout passes are completed
507            SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
508                @Override
509                public void run() {
510                    mFloatingLayout.showChild(new Runnable() {
511                        @Override
512                        public void run() {
513                            mHasFinishedAnimatingIn = true;
514                            mContactLoader.upgradeToFullContact();
515                        }
516                    });
517                }
518            });
519        }
520
521        @Override
522        public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
523            if (mLookupUri == null) {
524                Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
525            }
526            return new ContactLoader(getApplicationContext(), mLookupUri);
527        }
528    };
529
530    /** A type (e.g. Call/Addresses was clicked) */
531    private final OnClickListener mTypeViewClickListener = new OnClickListener() {
532        @Override
533        public void onClick(View view) {
534            final CheckableImageView actionView = (CheckableImageView)view;
535            final String mimeType = (String) actionView.getTag();
536            int index = mSortedActionMimeTypes.indexOf(mimeType);
537            mListPager.setCurrentItem(index, true);
538        }
539    };
540
541    private class ViewPagerAdapter extends FragmentPagerAdapter {
542        public ViewPagerAdapter(FragmentManager fragmentManager) {
543            super(fragmentManager);
544        }
545
546        @Override
547        public Fragment getItem(int position) {
548            QuickContactListFragment fragment = new QuickContactListFragment();
549            final String mimeType = mSortedActionMimeTypes.get(position);
550            final List<Action> actions = mActions.get(mimeType);
551            fragment.setActions(actions);
552            return fragment;
553        }
554
555        @Override
556        public int getCount() {
557            return mSortedActionMimeTypes.size();
558        }
559    }
560
561    private class PageChangeListener extends SimpleOnPageChangeListener {
562        @Override
563        public void onPageSelected(int position) {
564            final CheckableImageView actionView = getActionViewAt(position);
565            mTrackScroller.requestChildRectangleOnScreen(actionView,
566                    new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false);
567        }
568
569        @Override
570        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
571            final RelativeLayout.LayoutParams layoutParams =
572                    (RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams();
573            final int width = mSelectedTabRectangle.getWidth();
574            layoutParams.leftMargin = (int) ((position + positionOffset) * width);
575            mSelectedTabRectangle.setLayoutParams(layoutParams);
576        }
577    }
578
579    private final QuickContactListFragment.Listener mListFragmentListener =
580            new QuickContactListFragment.Listener() {
581        @Override
582        public void onOutsideClick() {
583            // If there is no background, we want to dismiss, because to the user it seems
584            // like he had touched outside. If the ViewPager is solid however, those taps
585            // must be ignored
586            final boolean isTransparent = mListPager.getBackground() == null;
587            if (isTransparent) handleOutsideTouch();
588        }
589
590        @Override
591        public void onItemClicked(final Action action, final boolean alternate) {
592            final Runnable startAppRunnable = new Runnable() {
593                @Override
594                public void run() {
595                    try {
596                        startActivity(alternate ? action.getAlternateIntent() : action.getIntent());
597                    } catch (ActivityNotFoundException e) {
598                        Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app,
599                                Toast.LENGTH_SHORT).show();
600                    }
601
602                    hide(false);
603                }
604            };
605            // Defer the action to make the window properly repaint
606            new Handler().post(startAppRunnable);
607        }
608    };
609}
610