QuickContactActivity.java revision 9758a92fac3e9f64892d893c992f6020d7fe3bfd
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.ContactPhotoManager;
22import com.android.contacts.R;
23import com.android.contacts.model.AccountTypeManager;
24import com.android.contacts.model.DataKind;
25import com.android.contacts.util.DataStatus;
26import com.android.contacts.util.ImageViewDrawableSetter;
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.BitmapFactory;
44import android.graphics.Rect;
45import android.graphics.drawable.Drawable;
46import android.net.Uri;
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.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.QuickContact;
58import android.provider.ContactsContract.RawContacts;
59import android.support.v13.app.FragmentPagerAdapter;
60import android.support.v4.view.ViewPager;
61import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
62import android.text.TextUtils;
63import android.util.Log;
64import android.view.MotionEvent;
65import android.view.View;
66import android.view.View.OnClickListener;
67import android.view.ViewGroup;
68import android.view.WindowManager;
69import android.widget.HorizontalScrollView;
70import android.widget.ImageButton;
71import android.widget.ImageView;
72import android.widget.RelativeLayout;
73import android.widget.TextView;
74import android.widget.Toast;
75
76import java.util.HashMap;
77import java.util.HashSet;
78import java.util.List;
79import java.util.Set;
80
81// TODO: Save selected tab index during rotation
82
83/**
84 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
85 * data asynchronously, and then shows a popup with details centered around
86 * {@link Intent#getSourceBounds()}.
87 */
88public class QuickContactActivity extends Activity {
89    private static final String TAG = "QuickContact";
90
91    private static final boolean TRACE_LAUNCH = false;
92    private static final String TRACE_TAG = "quickcontact";
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 final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
117
118    /**
119     * Keeps the default action per mimetype. Empty if no default actions are set
120     */
121    private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>();
122
123    /**
124     * Set of {@link Action} that are associated with the aggregate currently
125     * displayed by this dialog, represented as a map from {@link String}
126     * MIME-type to a list of {@link Action}.
127     */
128    private ActionMultiMap mActions = new ActionMultiMap();
129
130    /**
131     * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types.
132     *
133     * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
134     * in the order specified here.</p>
135     *
136     * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order
137     * specified here.</p>
138     *
139     * <p>The rest go between them, in the order in the array.</p>
140     */
141    private static final List<String> LEADING_MIMETYPES = Lists.newArrayList(
142            Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE);
143
144    /** See {@link #LEADING_MIMETYPES}. */
145    private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
146            StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
147
148    /** Id for the background loader */
149    private static final int LOADER_ID = 0;
150
151    @Override
152    protected void onCreate(Bundle icicle) {
153        super.onCreate(icicle);
154
155        // Show QuickContact in front of soft input
156        getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
157                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
158
159        setContentView(R.layout.quickcontact_activity);
160
161        mFloatingLayout = (FloatingChildLayout) findViewById(R.id.floating_layout);
162        mTrack = (ViewGroup) findViewById(R.id.track);
163        mTrackScroller = (HorizontalScrollView) findViewById(R.id.track_scroller);
164        mOpenDetailsButton = (ImageButton) findViewById(R.id.open_details_button);
165        mOpenDetailsPushLayerButton = (ImageButton) findViewById(R.id.open_details_push_layer);
166        mListPager = (ViewPager) findViewById(R.id.item_list_pager);
167        mSelectedTabRectangle = findViewById(R.id.selected_tab_rectangle);
168        mLineAfterTrack = findViewById(R.id.line_after_track);
169
170        mFloatingLayout.setOnOutsideTouchListener(new View.OnTouchListener() {
171            @Override
172            public boolean onTouch(View v, MotionEvent event) {
173                return handleOutsideTouch();
174            }
175        });
176
177        final OnClickListener openDetailsClickHandler = new OnClickListener() {
178            @Override
179            public void onClick(View v) {
180                final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
181                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
182                startActivity(intent);
183                hide(false);
184            }
185        };
186        mOpenDetailsButton.setOnClickListener(openDetailsClickHandler);
187        mOpenDetailsPushLayerButton.setOnClickListener(openDetailsClickHandler);
188        mListPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
189        mListPager.setOnPageChangeListener(new PageChangeListener());
190
191        show();
192    }
193
194    private void show() {
195
196        if (TRACE_LAUNCH) {
197            android.os.Debug.startMethodTracing(TRACE_TAG);
198        }
199
200        final Intent intent = getIntent();
201
202        Uri lookupUri = intent.getData();
203
204        // Check to see whether it comes from the old version.
205        if (LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
206            final long rawContactId = ContentUris.parseId(lookupUri);
207            lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
208                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
209        }
210
211        mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
212
213        // Read requested parameters for displaying
214        final Rect targetScreen = intent.getSourceBounds();
215        Preconditions.checkNotNull(targetScreen, "missing targetScreen");
216        mFloatingLayout.setChildTargetScreen(targetScreen);
217
218        mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
219
220        // find and prepare correct header view
221        mPhotoContainer = findViewById(R.id.photo_container);
222        setHeaderNameText(R.id.name, R.string.missing_name);
223
224        getLoaderManager().initLoader(LOADER_ID, null, mLoaderCallbacks);
225    }
226
227    private boolean handleOutsideTouch() {
228        if (!mHasFinishedAnimatingIn) return false;
229        if (mHasStartedAnimatingOut) return false;
230
231        mHasStartedAnimatingOut = true;
232        hide(true);
233        return true;
234    }
235
236    private void hide(boolean withAnimation) {
237        // cancel any pending queries
238        getLoaderManager().destroyLoader(LOADER_ID);
239
240        if (withAnimation) {
241            mFloatingLayout.hideChild(new Runnable() {
242                @Override
243                public void run() {
244                    finish();
245                }
246            });
247        } else {
248            mFloatingLayout.hideChild(null);
249            finish();
250        }
251    }
252
253    @Override
254    public void onBackPressed() {
255        hide(true);
256    }
257
258    /** Assign this string to the view if it is not empty. */
259    private void setHeaderNameText(int id, int resId) {
260        setHeaderNameText(id, getText(resId));
261    }
262
263    /** Assign this string to the view if it is not empty. */
264    private void setHeaderNameText(int id, CharSequence value) {
265        final View view = mPhotoContainer.findViewById(id);
266        if (view instanceof TextView) {
267            if (!TextUtils.isEmpty(value)) {
268                ((TextView)view).setText(value);
269            }
270        }
271    }
272
273    /**
274     * Check if the given MIME-type appears in the list of excluded MIME-types
275     * that the most-recent caller requested.
276     */
277    private boolean isMimeExcluded(String mimeType) {
278        if (mExcludeMimes == null) return false;
279        for (String excludedMime : mExcludeMimes) {
280            if (TextUtils.equals(excludedMime, mimeType)) {
281                return true;
282            }
283        }
284        return false;
285    }
286
287    /**
288     * Handle the result from the ContactLoader
289     */
290    private void bindData(ContactLoader.Result data) {
291        final ResolveCache cache = ResolveCache.getInstance(this);
292        final Context context = this;
293
294        mOpenDetailsButton.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? View.GONE
295                : View.VISIBLE);
296
297        mDefaultsMap.clear();
298
299        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
300                context.getApplicationContext());
301        final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
302        mPhotoSetter.setupContactPhoto(data, photoView);
303
304        for (Entity entity : data.getEntities()) {
305            final ContentValues entityValues = entity.getEntityValues();
306            final String accountType = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
307            final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
308            for (NamedContentValues subValue : entity.getSubValues()) {
309                final ContentValues entryValues = subValue.values;
310                final String mimeType = entryValues.getAsString(Data.MIMETYPE);
311
312                // Skip this data item if MIME-type excluded
313                if (isMimeExcluded(mimeType)) continue;
314
315                final long dataId = entryValues.getAsLong(Data._ID);
316                final Integer primary = entryValues.getAsInteger(Data.IS_PRIMARY);
317                final boolean isPrimary = primary != null && primary != 0;
318                final Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY);
319                final boolean isSuperPrimary = superPrimary != null && superPrimary != 0;
320
321                final DataKind kind =
322                        accountTypes.getKindOrFallback(accountType, dataSet, mimeType);
323
324                if (kind != null) {
325                    // Build an action for this data entry, find a mapping to a UI
326                    // element, build its summary from the cursor, and collect it
327                    // along with all others of this MIME-type.
328                    final Action action = new DataAction(context, mimeType, kind, dataId,
329                            entryValues);
330                    final boolean wasAdded = considerAdd(action, cache);
331                    if (wasAdded) {
332                        // Remember the default
333                        if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
334                            mDefaultsMap.put(mimeType, action);
335                        }
336                    }
337                }
338
339                // Handle Email rows with presence data as Im entry
340                final DataStatus status = data.getStatuses().get(dataId);
341                if (status != null && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
342                    final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet,
343                            Im.CONTENT_ITEM_TYPE);
344                    if (imKind != null) {
345                        final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE,
346                                imKind, dataId, entryValues);
347                        action.setPresence(status.getPresence());
348                        considerAdd(action, cache);
349                    }
350                }
351            }
352        }
353
354        // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
355        for (List<Action> actionChildren : mActions.values()) {
356            Collapser.collapseList(actionChildren);
357        }
358
359        setHeaderNameText(R.id.name, data.getDisplayName());
360
361        // All the mime-types to add.
362        final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
363        mSortedActionMimeTypes.clear();
364        // First, add LEADING_MIMETYPES, which are most common.
365        for (String mimeType : LEADING_MIMETYPES) {
366            if (containedTypes.contains(mimeType)) {
367                mSortedActionMimeTypes.add(mimeType);
368                containedTypes.remove(mimeType);
369            }
370        }
371
372        // Add all the remaining ones that are not TRAILING
373        for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) {
374            if (!TRAILING_MIMETYPES.contains(mimeType)) {
375                mSortedActionMimeTypes.add(mimeType);
376                containedTypes.remove(mimeType);
377            }
378        }
379
380        // Then, add TRAILING_MIMETYPES, which are least common.
381        for (String mimeType : TRAILING_MIMETYPES) {
382            if (containedTypes.contains(mimeType)) {
383                containedTypes.remove(mimeType);
384                mSortedActionMimeTypes.add(mimeType);
385            }
386        }
387
388        // Add buttons for each mimetype
389        mTrack.removeAllViews();
390        for (String mimeType : mSortedActionMimeTypes) {
391            final View actionView = inflateAction(mimeType, cache, mTrack);
392            mTrack.addView(actionView);
393        }
394
395        final boolean hasData = !mSortedActionMimeTypes.isEmpty();
396        mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE);
397        mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE);
398        mLineAfterTrack.setVisibility(hasData ? View.VISIBLE : View.GONE);
399        mListPager.setVisibility(hasData ? View.VISIBLE : View.GONE);
400    }
401
402    /**
403     * Consider adding the given {@link Action}, which will only happen if
404     * {@link PackageManager} finds an application to handle
405     * {@link Action#getIntent()}.
406     * @return true if action has been added
407     */
408    private boolean considerAdd(Action action, ResolveCache resolveCache) {
409        if (resolveCache.hasResolve(action)) {
410            mActions.put(action.getMimeType(), action);
411            return true;
412        }
413        return false;
414    }
415
416    /**
417     * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
418     * Will use the icon provided by the {@link DataKind}.
419     */
420    private View inflateAction(String mimeType, ResolveCache resolveCache, ViewGroup root) {
421        final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate(
422                R.layout.quickcontact_track_button, root, false);
423
424        List<Action> children = mActions.get(mimeType);
425        typeView.setTag(mimeType);
426        final Action firstInfo = children.get(0);
427
428        // Set icon and listen for clicks
429        final CharSequence descrip = resolveCache.getDescription(firstInfo);
430        final Drawable icon = resolveCache.getIcon(firstInfo);
431        typeView.setChecked(false);
432        typeView.setContentDescription(descrip);
433        typeView.setImageDrawable(icon);
434        typeView.setOnClickListener(mTypeViewClickListener);
435
436        return typeView;
437    }
438
439    private CheckableImageView getActionViewAt(int position) {
440        return (CheckableImageView) mTrack.getChildAt(position);
441    }
442
443    @Override
444    public void onAttachFragment(Fragment fragment) {
445        final QuickContactListFragment listFragment = (QuickContactListFragment) fragment;
446        listFragment.setListener(mListFragmentListener);
447    }
448
449    private LoaderCallbacks<ContactLoader.Result> mLoaderCallbacks =
450            new LoaderCallbacks<ContactLoader.Result>() {
451        @Override
452        public void onLoaderReset(Loader<ContactLoader.Result> loader) {
453        }
454
455        @Override
456        public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
457            if (isFinishing()) {
458                hide(false);
459                return;
460            }
461            if (data.isError()) {
462                // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
463                // should log the actual exception.
464                throw new IllegalStateException("Failed to load contact", data.getException());
465            }
466            if (data.isNotFound()) {
467                Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
468                Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
469                        Toast.LENGTH_LONG).show();
470                hide(false);
471                return;
472            }
473
474            bindData(data);
475
476            if (TRACE_LAUNCH) {
477                android.os.Debug.stopMethodTracing();
478            }
479
480            // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
481            // that the layout passes are completed
482            new Handler().post(new Runnable() {
483                @Override
484                public void run() {
485                    mFloatingLayout.showChild(new Runnable() {
486                        @Override
487                        public void run() {
488                            mHasFinishedAnimatingIn = true;
489                        }
490                    });
491                }
492            });
493        }
494
495        @Override
496        public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
497            if (mLookupUri == null) {
498                Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
499            }
500            return new ContactLoader(getApplicationContext(), mLookupUri);
501        }
502    };
503
504    /** A type (e.g. Call/Addresses was clicked) */
505    private final OnClickListener mTypeViewClickListener = new OnClickListener() {
506        @Override
507        public void onClick(View view) {
508            final CheckableImageView actionView = (CheckableImageView)view;
509            final String mimeType = (String) actionView.getTag();
510            int index = mSortedActionMimeTypes.indexOf(mimeType);
511            mListPager.setCurrentItem(index, true);
512        }
513    };
514
515    private class ViewPagerAdapter extends FragmentPagerAdapter {
516        public ViewPagerAdapter(FragmentManager fragmentManager) {
517            super(fragmentManager);
518        }
519
520        @Override
521        public Fragment getItem(int position) {
522            QuickContactListFragment fragment = new QuickContactListFragment();
523            final String mimeType = mSortedActionMimeTypes.get(position);
524            final List<Action> actions = mActions.get(mimeType);
525            fragment.setActions(actions);
526            return fragment;
527        }
528
529        @Override
530        public int getCount() {
531            return mSortedActionMimeTypes.size();
532        }
533    }
534
535    private class PageChangeListener extends SimpleOnPageChangeListener {
536        @Override
537        public void onPageSelected(int position) {
538            final CheckableImageView actionView = getActionViewAt(position);
539            mTrackScroller.requestChildRectangleOnScreen(actionView,
540                    new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false);
541        }
542
543        @Override
544        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
545            final RelativeLayout.LayoutParams layoutParams =
546                    (RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams();
547            final int width = mSelectedTabRectangle.getWidth();
548            layoutParams.leftMargin = (int) ((position + positionOffset) * width);
549            mSelectedTabRectangle.setLayoutParams(layoutParams);
550        }
551    }
552
553    private final QuickContactListFragment.Listener mListFragmentListener =
554            new QuickContactListFragment.Listener() {
555        @Override
556        public void onOutsideClick() {
557            // If there is no background, we want to dismiss, because to the user it seems
558            // like he had touched outside. If the ViewPager is solid however, those taps
559            // must be ignored
560            final boolean isTransparent = mListPager.getBackground() == null;
561            if (isTransparent) handleOutsideTouch();
562        }
563
564        @Override
565        public void onItemClicked(final Action action, final boolean alternate) {
566            final Runnable startAppRunnable = new Runnable() {
567                @Override
568                public void run() {
569                    try {
570                        startActivity(alternate ? action.getAlternateIntent() : action.getIntent());
571                    } catch (ActivityNotFoundException e) {
572                        Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app,
573                                Toast.LENGTH_SHORT).show();
574                    }
575
576                    hide(false);
577                }
578            };
579            // Defer the action to make the window properly repaint
580            new Handler().post(startAppRunnable);
581        }
582    };
583}
584