QuickContactActivity.java revision 71032f3fb7038995297666602773ae023c1351c4
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 android.app.Activity;
20import android.app.Fragment;
21import android.app.FragmentManager;
22import android.app.LoaderManager.LoaderCallbacks;
23import android.content.ActivityNotFoundException;
24import android.content.ContentUris;
25import android.content.Context;
26import android.content.Intent;
27import android.content.Loader;
28import android.content.pm.PackageManager;
29import android.graphics.Rect;
30import android.graphics.drawable.Drawable;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.Handler;
34import android.provider.ContactsContract.CommonDataKinds.Email;
35import android.provider.ContactsContract.CommonDataKinds.Phone;
36import android.provider.ContactsContract.CommonDataKinds.SipAddress;
37import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
38import android.provider.ContactsContract.CommonDataKinds.Website;
39import android.provider.ContactsContract.Contacts;
40import android.provider.ContactsContract.DisplayNameSources;
41import android.provider.ContactsContract.Intents.Insert;
42import android.provider.ContactsContract.Directory;
43import android.provider.ContactsContract.QuickContact;
44import android.provider.ContactsContract.RawContacts;
45import android.support.v13.app.FragmentPagerAdapter;
46import android.support.v4.view.PagerAdapter;
47import android.support.v4.view.ViewPager;
48import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
49import android.text.TextUtils;
50import android.util.Log;
51import android.view.MotionEvent;
52import android.view.View;
53import android.view.View.OnClickListener;
54import android.view.ViewGroup;
55import android.view.WindowManager;
56import android.widget.HorizontalScrollView;
57import android.widget.ImageView;
58import android.widget.LinearLayout;
59import android.widget.RelativeLayout;
60import android.widget.TextView;
61import android.widget.Toast;
62
63import com.android.contacts.ContactSaveService;
64import com.android.contacts.common.Collapser;
65import com.android.contacts.R;
66import com.android.contacts.common.model.AccountTypeManager;
67import com.android.contacts.common.model.Contact;
68import com.android.contacts.common.model.ContactLoader;
69import com.android.contacts.common.model.RawContact;
70import com.android.contacts.common.model.account.AccountType;
71import com.android.contacts.common.model.dataitem.DataItem;
72import com.android.contacts.common.model.dataitem.DataKind;
73import com.android.contacts.common.model.dataitem.EmailDataItem;
74import com.android.contacts.common.model.dataitem.ImDataItem;
75import com.android.contacts.common.util.Constants;
76import com.android.contacts.common.util.DataStatus;
77import com.android.contacts.common.util.UriUtils;
78import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry;
79import com.android.contacts.util.ImageViewDrawableSetter;
80import com.android.contacts.util.SchedulingUtils;
81import com.android.contacts.common.util.StopWatch;
82
83import com.google.common.base.Preconditions;
84import com.google.common.collect.Lists;
85
86import java.util.ArrayList;
87import java.util.HashMap;
88import java.util.HashSet;
89import java.util.List;
90import java.util.Set;
91
92// TODO: Save selected tab index during rotation
93
94/**
95 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
96 * data asynchronously, and then shows a popup with details centered around
97 * {@link Intent#getSourceBounds()}.
98 */
99public class QuickContactActivity extends Activity {
100    private static final String TAG = "QuickContact";
101
102    private static final boolean TRACE_LAUNCH = false;
103    private static final String TRACE_TAG = "quickcontact";
104    private static final int POST_DRAW_WAIT_DURATION = 60;
105    private static final boolean ENABLE_STOPWATCH = false;
106
107
108    @SuppressWarnings("deprecation")
109    private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
110
111    private Uri mLookupUri;
112    private String[] mExcludeMimes;
113    private List<String> mSortedActionMimeTypes = Lists.newArrayList();
114
115    private View mPhotoContainer;
116
117    private ImageView mPhotoView;
118    private ImageView mEditOrAddContactImage;
119    private ImageView mStarImage;
120    private ExpandingEntryCardView mCommunicationCard;
121
122    private Contact mContactData;
123    private ContactLoader mContactLoader;
124
125    private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
126
127    /**
128     * Keeps the default action per mimetype. Empty if no default actions are set
129     */
130    private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>();
131
132    /**
133     * Set of {@link Action} that are associated with the aggregate currently
134     * displayed by this dialog, represented as a map from {@link String}
135     * MIME-type to a list of {@link Action}.
136     */
137    private ActionMultiMap mActions = new ActionMultiMap();
138
139    /**
140     * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types.
141     *
142     * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
143     * in the order specified here.</p>
144     *
145     * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order
146     * specified here.</p>
147     *
148     * <p>The rest go between them, in the order in the array.</p>
149     */
150    private static final List<String> LEADING_MIMETYPES = Lists.newArrayList(
151            Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE);
152
153    /** See {@link #LEADING_MIMETYPES}. */
154    private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
155            StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
156
157    /** Id for the background loader */
158    private static final int LOADER_ID = 0;
159
160    private StopWatch mStopWatch = ENABLE_STOPWATCH
161            ? StopWatch.start("QuickContact") : StopWatch.getNullStopWatch();
162
163    final OnClickListener mEditContactClickHandler = new OnClickListener() {
164        @Override
165        public void onClick(View v) {
166            final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri);
167            mContactLoader.cacheResult();
168            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
169            startActivity(intent);
170        }
171    };
172
173    final OnClickListener mAddToContactsClickHandler = new OnClickListener() {
174        @Override
175        public void onClick(View v) {
176            if (mContactData == null) {
177                Log.e(TAG, "Empty contact data when trying to add to contact");
178                return;
179            }
180            final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
181            intent.setType(Contacts.CONTENT_ITEM_TYPE);
182
183            // Only pre-fill the name field if the provided display name is an organization
184            // name or better (e.g. structured name, nickname)
185            if (mContactData.getDisplayNameSource() >= DisplayNameSources.ORGANIZATION) {
186                intent.putExtra(Insert.NAME, mContactData.getDisplayName());
187            }
188            intent.putExtra(Insert.DATA, mContactData.getContentValues());
189            startActivity(intent);
190        }
191    };
192
193    final OnClickListener mEntryClickHandler = new OnClickListener() {
194        @Override
195        public void onClick(View v) {
196            Log.i(TAG, "mEntryClickHandler onClick");
197            Object intent = v.getTag();
198            if (intent == null || !(intent instanceof Intent)) {
199                return;
200            }
201            startActivity((Intent) intent);
202        }
203    };
204
205    @Override
206    protected void onCreate(Bundle icicle) {
207        mStopWatch.lap("c"); // create start
208        super.onCreate(icicle);
209
210        mStopWatch.lap("sc"); // super.onCreate
211
212        if (TRACE_LAUNCH) android.os.Debug.startMethodTracing(TRACE_TAG);
213
214        // Parse intent
215        final Intent intent = getIntent();
216
217        Uri lookupUri = intent.getData();
218
219        // Check to see whether it comes from the old version.
220        if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
221            final long rawContactId = ContentUris.parseId(lookupUri);
222            lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
223                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
224        }
225
226        mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
227
228        mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
229
230        mStopWatch.lap("i"); // intent parsed
231
232        mContactLoader = (ContactLoader) getLoaderManager().initLoader(
233                LOADER_ID, null, mLoaderCallbacks);
234
235        mStopWatch.lap("ld"); // loader started
236
237        // Show QuickContact in front of soft input
238        getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
239                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
240
241        setContentView(R.layout.quickcontact_activity);
242
243        mStopWatch.lap("l"); // layout inflated
244
245        mEditOrAddContactImage = (ImageView) findViewById(R.id.contact_edit_image);
246        mStarImage = (ImageView) findViewById(R.id.quickcontact_star_button);
247        mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
248        mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title));
249
250        mEditOrAddContactImage.setOnClickListener(mEditContactClickHandler);
251        mCommunicationCard.setOnClickListener(mEntryClickHandler);
252
253        // find and prepare correct header view
254        mPhotoContainer = findViewById(R.id.photo_container);
255
256        setHeaderNameText(R.id.name, R.string.missing_name);
257
258        mPhotoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
259        mPhotoView.setOnClickListener(mEditContactClickHandler);
260
261        mStopWatch.lap("v"); // view initialized
262
263        // TODO: Use some sort of fading in for the layout and content during animation
264        /*SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
265            @Override
266            public void run() {
267                mFloatingLayout.fadeInBackground();
268            }
269        });*/
270
271        mStopWatch.lap("cf"); // onCreate finished
272    }
273
274    /** Assign this string to the view if it is not empty. */
275    private void setHeaderNameText(int id, int resId) {
276        setHeaderNameText(id, getText(resId));
277    }
278
279    /** Assign this string to the view if it is not empty. */
280    private void setHeaderNameText(int id, CharSequence value) {
281        final View view = mPhotoContainer.findViewById(id);
282        if (view instanceof TextView) {
283            if (!TextUtils.isEmpty(value)) {
284                ((TextView)view).setText(value);
285            }
286        }
287    }
288
289    /**
290     * Check if the given MIME-type appears in the list of excluded MIME-types
291     * that the most-recent caller requested.
292     */
293    private boolean isMimeExcluded(String mimeType) {
294        if (mExcludeMimes == null) return false;
295        for (String excludedMime : mExcludeMimes) {
296            if (TextUtils.equals(excludedMime, mimeType)) {
297                return true;
298            }
299        }
300        return false;
301    }
302
303    /**
304     * Handle the result from the ContactLoader
305     */
306    private void bindData(Contact data) {
307        mContactData = data;
308        final ResolveCache cache = ResolveCache.getInstance(this);
309        final Context context = this;
310
311        mEditOrAddContactImage.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ?
312                View.GONE : View.VISIBLE);
313        final boolean isStarred = data.getStarred();
314        if (isStarred) {
315            mStarImage.setImageResource(R.drawable.ic_favorite_on_lt);
316            mStarImage.setContentDescription(
317                getResources().getString(R.string.menu_removeStar));
318        } else {
319            mStarImage.setImageResource(R.drawable.ic_favorite_off_lt);
320            mStarImage.setContentDescription(
321                getResources().getString(R.string.menu_addStar));
322        }
323        final Uri lookupUri = data.getLookupUri();
324
325        // If this is a json encoded URI, there is no local contact to star
326        if (UriUtils.isEncodedContactUri(lookupUri)) {
327            mStarImage.setVisibility(View.GONE);
328
329            // If directory export support is not allowed, then don't allow the user to add
330            // to contacts
331            if (mContactData.getDirectoryExportSupport() == Directory.EXPORT_SUPPORT_NONE) {
332                configureHeaderClickActions(false);
333            } else {
334                configureHeaderClickActions(true);
335            }
336        } else {
337            configureHeaderClickActions(false);
338            mStarImage.setVisibility(View.VISIBLE);
339            mStarImage.setOnClickListener(new OnClickListener() {
340                @Override
341                public void onClick(View view) {
342                    // Toggle "starred" state
343                    // Make sure there is a contact
344                    if (lookupUri != null) {
345                        // Changes the state of the image already before sending updates to the
346                        // database
347                        if (isStarred) {
348                            mStarImage.setImageResource(R.drawable.ic_favorite_off_lt);
349                        } else {
350                            mStarImage.setImageResource(R.drawable.ic_favorite_on_lt);
351                        }
352
353                        // Now perform the real save
354                        final Intent intent = ContactSaveService.createSetStarredIntent(context,
355                                lookupUri, !isStarred);
356                        context.startService(intent);
357                    }
358                }
359            });
360        }
361
362        mDefaultsMap.clear();
363
364        mStopWatch.lap("sph"); // Start photo setting
365
366        mPhotoSetter.setupContactPhoto(data, mPhotoView);
367
368        mStopWatch.lap("ph"); // Photo set
369
370        for (RawContact rawContact : data.getRawContacts()) {
371            for (DataItem dataItem : rawContact.getDataItems()) {
372                final String mimeType = dataItem.getMimeType();
373                final AccountType accountType = rawContact.getAccountType(this);
374                final DataKind dataKind = AccountTypeManager.getInstance(this)
375                        .getKindOrFallback(accountType, mimeType);
376
377                // Skip this data item if MIME-type excluded
378                if (isMimeExcluded(mimeType)) continue;
379
380                final long dataId = dataItem.getId();
381                final boolean isPrimary = dataItem.isPrimary();
382                final boolean isSuperPrimary = dataItem.isSuperPrimary();
383
384                if (dataKind != null) {
385                    // Build an action for this data entry, find a mapping to a UI
386                    // element, build its summary from the cursor, and collect it
387                    // along with all others of this MIME-type.
388                    final Action action = new DataAction(context, dataItem, dataKind);
389                    final boolean wasAdded = considerAdd(action, cache, isSuperPrimary);
390                    if (wasAdded) {
391                        // Remember the default
392                        if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
393                            mDefaultsMap.put(mimeType, action);
394                        }
395                    }
396                }
397
398                // Handle Email rows with presence data as Im entry
399                final DataStatus status = data.getStatuses().get(dataId);
400                if (status != null && dataItem instanceof EmailDataItem) {
401                    final EmailDataItem email = (EmailDataItem) dataItem;
402                    final ImDataItem im = ImDataItem.createFromEmail(email);
403                    if (dataKind != null) {
404                        final DataAction action = new DataAction(context, im, dataKind);
405                        action.setPresence(status.getPresence());
406                        considerAdd(action, cache, isSuperPrimary);
407                    }
408                }
409            }
410        }
411
412        mStopWatch.lap("e"); // Entities inflated
413
414        // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
415        for (List<Action> actionChildren : mActions.values()) {
416            Collapser.collapseList(actionChildren);
417        }
418
419        mStopWatch.lap("c"); // List collapsed
420
421        setHeaderNameText(R.id.name, data.getDisplayName());
422
423        // List of Entry that makes up the ExpandingEntryCardView
424        final List<Entry> entries = new ArrayList<>();
425        // All the mime-types to add.
426        final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
427        mSortedActionMimeTypes.clear();
428        // First, add LEADING_MIMETYPES, which are most common.
429        for (String mimeType : LEADING_MIMETYPES) {
430            if (containedTypes.contains(mimeType)) {
431                mSortedActionMimeTypes.add(mimeType);
432                containedTypes.remove(mimeType);
433                entries.addAll(actionsToEntries(mActions.get(mimeType)));
434            }
435        }
436
437        // Add all the remaining ones that are not TRAILING
438        for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) {
439            if (!TRAILING_MIMETYPES.contains(mimeType)) {
440                mSortedActionMimeTypes.add(mimeType);
441                containedTypes.remove(mimeType);
442                entries.addAll(actionsToEntries(mActions.get(mimeType)));
443            }
444        }
445
446        // Then, add TRAILING_MIMETYPES, which are least common.
447        for (String mimeType : TRAILING_MIMETYPES) {
448            if (containedTypes.contains(mimeType)) {
449                containedTypes.remove(mimeType);
450                mSortedActionMimeTypes.add(mimeType);
451                entries.addAll(actionsToEntries(mActions.get(mimeType)));
452            }
453        }
454        mCommunicationCard.initialize(entries, /* numInitialVisibleEntries = */ 2,
455                /* isExpanded = */ false, /* themeColor = */ 0);
456
457        final boolean hasData = !mSortedActionMimeTypes.isEmpty();
458        mCommunicationCard.setVisibility(hasData ? View.VISIBLE: View.GONE);
459    }
460
461    /**
462     * Consider adding the given {@link Action}, which will only happen if
463     * {@link PackageManager} finds an application to handle
464     * {@link Action#getIntent()}.
465     * @param action the action to handle
466     * @param resolveCache cache of applications that can handle actions
467     * @param front indicates whether to add the action to the front of the list
468     * @return true if action has been added
469     */
470    private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) {
471        if (resolveCache.hasResolve(action)) {
472            mActions.put(action.getMimeType(), action, front);
473            return true;
474        }
475        return false;
476    }
477
478    /**
479     * Bind the correct image resource and click handlers to the header views
480     *
481     * @param canAdd Whether or not the user can directly add information in this quick contact
482     * to their local contacts
483     */
484    private void configureHeaderClickActions(boolean canAdd) {
485        if (canAdd) {
486            mEditOrAddContactImage.setImageResource(R.drawable.ic_person_add_24dp);
487            mEditOrAddContactImage.setOnClickListener(mAddToContactsClickHandler);
488            mPhotoView.setOnClickListener(mAddToContactsClickHandler);
489        } else {
490            mEditOrAddContactImage.setImageResource(R.drawable.ic_create_24dp);
491            mEditOrAddContactImage.setOnClickListener(mEditContactClickHandler);
492            mPhotoView.setOnClickListener(mEditContactClickHandler);
493        }
494    }
495
496    /**
497     * Converts a list of Action into a list of Entry
498     * @param actions The list of Action to convert
499     * @return The converted list of Entry
500     */
501    private List<Entry> actionsToEntries(List<Action> actions) {
502        List<Entry> entries = new ArrayList<>();
503        for (Action action :  actions) {
504            entries.add(new Entry(ResolveCache.getInstance(this).getIcon(action),
505                    action.getMimeType(), action.getSubtitle().toString(),
506                    action.getBody().toString(), action.getIntent(), /* isEditable= */ false));
507        }
508        return entries;
509    }
510
511    private LoaderCallbacks<Contact> mLoaderCallbacks =
512            new LoaderCallbacks<Contact>() {
513        @Override
514        public void onLoaderReset(Loader<Contact> loader) {
515        }
516
517        @Override
518        public void onLoadFinished(Loader<Contact> loader, Contact data) {
519            mStopWatch.lap("lf"); // onLoadFinished
520            if (isFinishing()) {
521                return;
522            }
523            if (data.isError()) {
524                // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
525                // should log the actual exception.
526                throw new IllegalStateException("Failed to load contact", data.getException());
527            }
528            if (data.isNotFound()) {
529                Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
530                Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
531                        Toast.LENGTH_LONG).show();
532                return;
533            }
534
535            bindData(data);
536
537            mStopWatch.lap("bd"); // bindData finished
538
539            if (TRACE_LAUNCH) android.os.Debug.stopMethodTracing();
540            if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
541                Log.d(Constants.PERFORMANCE_TAG, "QuickContact shown");
542            }
543
544            // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
545            // that the layout passes are completed
546            // TODO: Add animation here
547            /*SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
548                @Override
549                public void run() {
550                    mFloatingLayout.showContent(new Runnable() {
551                        @Override
552                        public void run() {
553                            mContactLoader.upgradeToFullContact();
554                        }
555                    });
556                }
557            });*/
558            mStopWatch.stopAndLog(TAG, 0);
559            mStopWatch = StopWatch.getNullStopWatch(); // We're done with it.
560        }
561
562        @Override
563        public Loader<Contact> onCreateLoader(int id, Bundle args) {
564            if (mLookupUri == null) {
565                Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
566            }
567            return new ContactLoader(getApplicationContext(), mLookupUri,
568                    false /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/,
569                    false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/);
570        }
571    };
572}
573