1/*
2 * Copyright (C) 2010 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.detail;
18
19import android.app.Activity;
20import android.app.Fragment;
21import android.app.SearchManager;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.res.Resources;
27import android.graphics.drawable.Drawable;
28import android.net.ParseException;
29import android.net.Uri;
30import android.net.WebAddress;
31import android.os.Bundle;
32import android.os.Parcelable;
33import android.os.RemoteException;
34import android.os.ServiceManager;
35import android.provider.ContactsContract;
36import android.provider.ContactsContract.CommonDataKinds.Email;
37import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
38import android.provider.ContactsContract.CommonDataKinds.Im;
39import android.provider.ContactsContract.CommonDataKinds.Phone;
40import android.provider.ContactsContract.Contacts;
41import android.provider.ContactsContract.Data;
42import android.provider.ContactsContract.Directory;
43import android.provider.ContactsContract.DisplayNameSources;
44import android.provider.ContactsContract.StatusUpdates;
45import android.text.TextUtils;
46import android.util.Log;
47import android.view.ContextMenu;
48import android.view.ContextMenu.ContextMenuInfo;
49import android.view.DragEvent;
50import android.view.KeyEvent;
51import android.view.LayoutInflater;
52import android.view.MenuItem;
53import android.view.MotionEvent;
54import android.view.View;
55import android.view.View.OnClickListener;
56import android.view.View.OnDragListener;
57import android.view.View.OnTouchListener;
58import android.view.ViewGroup;
59import android.widget.AbsListView.OnScrollListener;
60import android.widget.AdapterView;
61import android.widget.AdapterView.AdapterContextMenuInfo;
62import android.widget.AdapterView.OnItemClickListener;
63import android.widget.BaseAdapter;
64import android.widget.Button;
65import android.widget.ImageView;
66import android.widget.ListAdapter;
67import android.widget.ListPopupWindow;
68import android.widget.ListView;
69import android.widget.TextView;
70
71import com.android.contacts.Collapser;
72import com.android.contacts.Collapser.Collapsible;
73import com.android.contacts.ContactPresenceIconUtil;
74import com.android.contacts.ContactSaveService;
75import com.android.contacts.ContactsUtils;
76import com.android.contacts.GroupMetaData;
77import com.android.contacts.R;
78import com.android.contacts.TypePrecedence;
79import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
80import com.android.contacts.editor.SelectAccountDialogFragment;
81import com.android.contacts.model.AccountTypeManager;
82import com.android.contacts.model.Contact;
83import com.android.contacts.model.RawContact;
84import com.android.contacts.model.RawContactDelta;
85import com.android.contacts.model.RawContactDelta.ValuesDelta;
86import com.android.contacts.model.RawContactDeltaList;
87import com.android.contacts.model.RawContactModifier;
88import com.android.contacts.model.account.AccountType;
89import com.android.contacts.model.account.AccountType.EditType;
90import com.android.contacts.model.account.AccountWithDataSet;
91import com.android.contacts.model.dataitem.DataItem;
92import com.android.contacts.model.dataitem.DataKind;
93import com.android.contacts.model.dataitem.EmailDataItem;
94import com.android.contacts.model.dataitem.EventDataItem;
95import com.android.contacts.model.dataitem.GroupMembershipDataItem;
96import com.android.contacts.model.dataitem.ImDataItem;
97import com.android.contacts.model.dataitem.NicknameDataItem;
98import com.android.contacts.model.dataitem.NoteDataItem;
99import com.android.contacts.model.dataitem.OrganizationDataItem;
100import com.android.contacts.model.dataitem.PhoneDataItem;
101import com.android.contacts.model.dataitem.RelationDataItem;
102import com.android.contacts.model.dataitem.SipAddressDataItem;
103import com.android.contacts.model.dataitem.StructuredNameDataItem;
104import com.android.contacts.model.dataitem.StructuredPostalDataItem;
105import com.android.contacts.model.dataitem.WebsiteDataItem;
106import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
107import com.android.contacts.util.ClipboardUtils;
108import com.android.contacts.util.Constants;
109import com.android.contacts.util.DataStatus;
110import com.android.contacts.util.DateUtils;
111import com.android.contacts.util.PhoneCapabilityTester;
112import com.android.contacts.util.StructuredPostalUtils;
113import com.android.internal.telephony.ITelephony;
114import com.google.common.annotations.VisibleForTesting;
115import com.google.common.collect.Iterables;
116
117import java.util.ArrayList;
118import java.util.Collections;
119import java.util.HashMap;
120import java.util.List;
121import java.util.Map;
122
123public class ContactDetailFragment extends Fragment implements FragmentKeyListener,
124        SelectAccountDialogFragment.Listener, OnItemClickListener {
125
126    private static final String TAG = "ContactDetailFragment";
127
128    private interface ContextMenuIds {
129        static final int COPY_TEXT = 0;
130        static final int CLEAR_DEFAULT = 1;
131        static final int SET_DEFAULT = 2;
132    }
133
134    private static final String KEY_CONTACT_URI = "contactUri";
135    private static final String KEY_LIST_STATE = "liststate";
136
137    private Context mContext;
138    private View mView;
139    private OnScrollListener mVerticalScrollListener;
140    private Uri mLookupUri;
141    private Listener mListener;
142
143    private Contact mContactData;
144    private ViewGroup mStaticPhotoContainer;
145    private View mPhotoTouchOverlay;
146    private ListView mListView;
147    private ViewAdapter mAdapter;
148    private Uri mPrimaryPhoneUri = null;
149    private ViewEntryDimensions mViewEntryDimensions;
150
151    private final ContactDetailPhotoSetter mPhotoSetter = new ContactDetailPhotoSetter();
152
153    private Button mQuickFixButton;
154    private QuickFix mQuickFix;
155    private String mDefaultCountryIso;
156    private boolean mContactHasSocialUpdates;
157    private boolean mShowStaticPhoto = true;
158
159    private final QuickFix[] mPotentialQuickFixes = new QuickFix[] {
160            new MakeLocalCopyQuickFix(),
161            new AddToMyContactsQuickFix()
162    };
163
164    /**
165     * Device capability: Set during buildEntries and used in the long-press context menu
166     */
167    private boolean mHasPhone;
168
169    /**
170     * Device capability: Set during buildEntries and used in the long-press context menu
171     */
172    private boolean mHasSms;
173
174    /**
175     * Device capability: Set during buildEntries and used in the long-press context menu
176     */
177    private boolean mHasSip;
178
179    /**
180     * The view shown if the detail list is empty.
181     * We set this to the list view when first bind the adapter, so that it won't be shown while
182     * we're loading data.
183     */
184    private View mEmptyView;
185
186    /**
187     * Saved state of the {@link ListView}. This must be saved and applied to the {@ListView} only
188     * when the adapter has been populated again.
189     */
190    private Parcelable mListState;
191
192    /**
193     * Lists of specific types of entries to be shown in contact details.
194     */
195    private ArrayList<DetailViewEntry> mPhoneEntries = new ArrayList<DetailViewEntry>();
196    private ArrayList<DetailViewEntry> mSmsEntries = new ArrayList<DetailViewEntry>();
197    private ArrayList<DetailViewEntry> mEmailEntries = new ArrayList<DetailViewEntry>();
198    private ArrayList<DetailViewEntry> mPostalEntries = new ArrayList<DetailViewEntry>();
199    private ArrayList<DetailViewEntry> mImEntries = new ArrayList<DetailViewEntry>();
200    private ArrayList<DetailViewEntry> mNicknameEntries = new ArrayList<DetailViewEntry>();
201    private ArrayList<DetailViewEntry> mGroupEntries = new ArrayList<DetailViewEntry>();
202    private ArrayList<DetailViewEntry> mRelationEntries = new ArrayList<DetailViewEntry>();
203    private ArrayList<DetailViewEntry> mNoteEntries = new ArrayList<DetailViewEntry>();
204    private ArrayList<DetailViewEntry> mWebsiteEntries = new ArrayList<DetailViewEntry>();
205    private ArrayList<DetailViewEntry> mSipEntries = new ArrayList<DetailViewEntry>();
206    private ArrayList<DetailViewEntry> mEventEntries = new ArrayList<DetailViewEntry>();
207    private final Map<AccountType, List<DetailViewEntry>> mOtherEntriesMap =
208            new HashMap<AccountType, List<DetailViewEntry>>();
209    private ArrayList<ViewEntry> mAllEntries = new ArrayList<ViewEntry>();
210    private LayoutInflater mInflater;
211
212    private boolean mIsUniqueNumber;
213    private boolean mIsUniqueEmail;
214
215    private ListPopupWindow mPopup;
216
217    /**
218     * This is to forward touch events to the list view to enable users to scroll the list view
219     * from the blank area underneath the static photo when the layout with static photo is used.
220     */
221    private OnTouchListener mForwardTouchToListView = new OnTouchListener() {
222        @Override
223        public boolean onTouch(View v, MotionEvent event) {
224            if (mListView != null) {
225                mListView.dispatchTouchEvent(event);
226                return true;
227            }
228            return false;
229        }
230    };
231
232    /**
233     * This is to forward drag events to the list view to enable users to scroll the list view
234     * from the blank area underneath the static photo when the layout with static photo is used.
235     */
236    private OnDragListener mForwardDragToListView = new OnDragListener() {
237        @Override
238        public boolean onDrag(View v, DragEvent event) {
239            if (mListView != null) {
240                mListView.dispatchDragEvent(event);
241                return true;
242            }
243            return false;
244        }
245    };
246
247    public ContactDetailFragment() {
248        // Explicit constructor for inflation
249    }
250
251    @Override
252    public void onCreate(Bundle savedInstanceState) {
253        super.onCreate(savedInstanceState);
254        if (savedInstanceState != null) {
255            mLookupUri = savedInstanceState.getParcelable(KEY_CONTACT_URI);
256            mListState = savedInstanceState.getParcelable(KEY_LIST_STATE);
257        }
258    }
259
260    @Override
261    public void onSaveInstanceState(Bundle outState) {
262        super.onSaveInstanceState(outState);
263        outState.putParcelable(KEY_CONTACT_URI, mLookupUri);
264        if (mListView != null) {
265            outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState());
266        }
267    }
268
269    @Override
270    public void onPause() {
271        dismissPopupIfShown();
272        super.onPause();
273    }
274
275    @Override
276    public void onResume() {
277        super.onResume();
278    }
279
280    @Override
281    public void onAttach(Activity activity) {
282        super.onAttach(activity);
283        mContext = activity;
284        mDefaultCountryIso = ContactsUtils.getCurrentCountryIso(mContext);
285        mViewEntryDimensions = new ViewEntryDimensions(mContext.getResources());
286    }
287
288    @Override
289    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
290        mView = inflater.inflate(R.layout.contact_detail_fragment, container, false);
291        // Set the touch and drag listener to forward the event to the mListView so that
292        // vertical scrolling can happen from outside of the list view.
293        mView.setOnTouchListener(mForwardTouchToListView);
294        mView.setOnDragListener(mForwardDragToListView);
295
296        mInflater = inflater;
297
298        mStaticPhotoContainer = (ViewGroup) mView.findViewById(R.id.static_photo_container);
299        mPhotoTouchOverlay = mView.findViewById(R.id.photo_touch_intercept_overlay);
300
301        mListView = (ListView) mView.findViewById(android.R.id.list);
302        mListView.setOnItemClickListener(this);
303        mListView.setItemsCanFocus(true);
304        mListView.setOnScrollListener(mVerticalScrollListener);
305
306        // Don't set it to mListView yet.  We do so later when we bind the adapter.
307        mEmptyView = mView.findViewById(android.R.id.empty);
308
309        mQuickFixButton = (Button) mView.findViewById(R.id.contact_quick_fix);
310        mQuickFixButton.setOnClickListener(new OnClickListener() {
311            @Override
312            public void onClick(View v) {
313                if (mQuickFix != null) {
314                    mQuickFix.execute();
315                }
316            }
317        });
318
319        mView.setVisibility(View.INVISIBLE);
320
321        if (mContactData != null) {
322            bindData();
323        }
324
325        return mView;
326    }
327
328    public void setListener(Listener value) {
329        mListener = value;
330    }
331
332    protected Context getContext() {
333        return mContext;
334    }
335
336    protected Listener getListener() {
337        return mListener;
338    }
339
340    protected Contact getContactData() {
341        return mContactData;
342    }
343
344    public void setVerticalScrollListener(OnScrollListener listener) {
345        mVerticalScrollListener = listener;
346    }
347
348    public Uri getUri() {
349        return mLookupUri;
350    }
351
352    /**
353     * Sets whether the static contact photo (that is not in a scrolling region), should be shown
354     * or not.
355     */
356    public void setShowStaticPhoto(boolean showPhoto) {
357        mShowStaticPhoto = showPhoto;
358    }
359
360    /**
361     * Shows the contact detail with a message indicating there are no contact details.
362     */
363    public void showEmptyState() {
364        setData(null, null);
365    }
366
367    public void setData(Uri lookupUri, Contact result) {
368        mLookupUri = lookupUri;
369        mContactData = result;
370        bindData();
371    }
372
373    /**
374     * Reset the list adapter in this {@link Fragment} to get rid of any saved scroll position
375     * from a previous contact.
376     */
377    public void resetAdapter() {
378        if (mListView != null) {
379            mListView.setAdapter(mAdapter);
380        }
381    }
382
383    /**
384     * Returns the top coordinate of the first item in the {@link ListView}. If the first item
385     * in the {@link ListView} is not visible or there are no children in the list, then return
386     * Integer.MIN_VALUE. Note that the returned value will be <= 0 because the first item in the
387     * list cannot have a positive offset.
388     */
389    public int getFirstListItemOffset() {
390        return ContactDetailDisplayUtils.getFirstListItemOffset(mListView);
391    }
392
393    /**
394     * Tries to scroll the first item to the given offset (this can be a no-op if the list is
395     * already in the correct position).
396     * @param offset which should be <= 0
397     */
398    public void requestToMoveToOffset(int offset) {
399        ContactDetailDisplayUtils.requestToMoveToOffset(mListView, offset);
400    }
401
402    protected void bindData() {
403        if (mView == null) {
404            return;
405        }
406
407        if (isAdded()) {
408            getActivity().invalidateOptionsMenu();
409        }
410
411        if (mContactData == null) {
412            mView.setVisibility(View.INVISIBLE);
413            if (mStaticPhotoContainer != null) {
414                mStaticPhotoContainer.setVisibility(View.GONE);
415            }
416            mAllEntries.clear();
417            if (mAdapter != null) {
418                mAdapter.notifyDataSetChanged();
419            }
420            return;
421        }
422
423        // Figure out if the contact has social updates or not
424        mContactHasSocialUpdates = !mContactData.getStreamItems().isEmpty();
425
426        // Setup the photo if applicable
427        if (mStaticPhotoContainer != null) {
428            // The presence of a static photo container is not sufficient to determine whether or
429            // not we should show the photo. Check the mShowStaticPhoto flag which can be set by an
430            // outside class depending on screen size, layout, and whether the contact has social
431            // updates or not.
432            if (mShowStaticPhoto) {
433                mStaticPhotoContainer.setVisibility(View.VISIBLE);
434                final ImageView photoView = (ImageView) mStaticPhotoContainer.findViewById(
435                        R.id.photo);
436                final boolean expandPhotoOnClick = mContactData.getPhotoUri() != null;
437                final OnClickListener listener = mPhotoSetter.setupContactPhotoForClick(
438                        mContext, mContactData, photoView, expandPhotoOnClick);
439                if (mPhotoTouchOverlay != null) {
440                    mPhotoTouchOverlay.setVisibility(View.VISIBLE);
441                    if (expandPhotoOnClick || mContactData.isWritableContact(mContext)) {
442                        mPhotoTouchOverlay.setOnClickListener(listener);
443                    } else {
444                        mPhotoTouchOverlay.setClickable(false);
445                    }
446                }
447            } else {
448                mStaticPhotoContainer.setVisibility(View.GONE);
449            }
450        }
451
452        // Build up the contact entries
453        buildEntries();
454
455        // Collapse similar data items for select {@link DataKind}s.
456        Collapser.collapseList(mPhoneEntries);
457        Collapser.collapseList(mSmsEntries);
458        Collapser.collapseList(mEmailEntries);
459        Collapser.collapseList(mPostalEntries);
460        Collapser.collapseList(mImEntries);
461        Collapser.collapseList(mEventEntries);
462
463        mIsUniqueNumber = mPhoneEntries.size() == 1;
464        mIsUniqueEmail = mEmailEntries.size() == 1;
465
466        // Make one aggregated list of all entries for display to the user.
467        setupFlattenedList();
468
469        if (mAdapter == null) {
470            mAdapter = new ViewAdapter();
471            mListView.setAdapter(mAdapter);
472        }
473
474        // Restore {@link ListView} state if applicable because the adapter is now populated.
475        if (mListState != null) {
476            mListView.onRestoreInstanceState(mListState);
477            mListState = null;
478        }
479
480        mAdapter.notifyDataSetChanged();
481
482        mListView.setEmptyView(mEmptyView);
483
484        configureQuickFix();
485
486        mView.setVisibility(View.VISIBLE);
487    }
488
489    /*
490     * Sets {@link #mQuickFix} to a useful action and configures the visibility of
491     * {@link #mQuickFixButton}
492     */
493    private void configureQuickFix() {
494        mQuickFix = null;
495
496        for (QuickFix fix : mPotentialQuickFixes) {
497            if (fix.isApplicable()) {
498                mQuickFix = fix;
499                break;
500            }
501        }
502
503        // Configure the button
504        if (mQuickFix == null) {
505            mQuickFixButton.setVisibility(View.GONE);
506        } else {
507            mQuickFixButton.setVisibility(View.VISIBLE);
508            mQuickFixButton.setText(mQuickFix.getTitle());
509        }
510    }
511
512    /** @return default group id or -1 if no group or several groups are marked as default */
513    private long getDefaultGroupId(List<GroupMetaData> groups) {
514        long defaultGroupId = -1;
515        for (GroupMetaData group : groups) {
516            if (group.isDefaultGroup()) {
517                // two default groups? return neither
518                if (defaultGroupId != -1) return -1;
519                defaultGroupId = group.getGroupId();
520            }
521        }
522        return defaultGroupId;
523    }
524
525    /**
526     * Build up the entries to display on the screen.
527     */
528    private final void buildEntries() {
529        mHasPhone = PhoneCapabilityTester.isPhone(mContext);
530        mHasSms = PhoneCapabilityTester.isSmsIntentRegistered(mContext);
531        mHasSip = PhoneCapabilityTester.isSipPhone(mContext);
532
533        // Clear out the old entries
534        mAllEntries.clear();
535
536        mPrimaryPhoneUri = null;
537
538        // Build up method entries
539        if (mContactData == null) {
540            return;
541        }
542
543        ArrayList<String> groups = new ArrayList<String>();
544        for (RawContact rawContact: mContactData.getRawContacts()) {
545            final long rawContactId = rawContact.getId();
546            for (DataItem dataItem : rawContact.getDataItems()) {
547                dataItem.setRawContactId(rawContactId);
548
549                if (dataItem.getMimeType() == null) continue;
550
551                if (dataItem instanceof GroupMembershipDataItem) {
552                    GroupMembershipDataItem groupMembership =
553                            (GroupMembershipDataItem) dataItem;
554                    Long groupId = groupMembership.getGroupRowId();
555                    if (groupId != null) {
556                        handleGroupMembership(groups, mContactData.getGroupMetaData(), groupId);
557                    }
558                    continue;
559                }
560
561                final DataKind kind = dataItem.getDataKind();
562                if (kind == null) continue;
563
564                final DetailViewEntry entry = DetailViewEntry.fromValues(mContext, dataItem,
565                        mContactData.isDirectoryEntry(), mContactData.getDirectoryId());
566                entry.maxLines = kind.maxLinesForDisplay;
567
568                final boolean hasData = !TextUtils.isEmpty(entry.data);
569                final boolean isSuperPrimary = dataItem.isSuperPrimary();
570
571                if (dataItem instanceof StructuredNameDataItem) {
572                    // Always ignore the name. It is shown in the header if set
573                } else if (dataItem instanceof PhoneDataItem && hasData) {
574                    PhoneDataItem phone = (PhoneDataItem) dataItem;
575                    // Build phone entries
576                    entry.data = phone.getFormattedPhoneNumber();
577                    final Intent phoneIntent = mHasPhone ?
578                            ContactsUtils.getCallIntent(entry.data) : null;
579                    final Intent smsIntent = mHasSms ? new Intent(Intent.ACTION_SENDTO,
580                            Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null)) : null;
581
582                    // Configure Icons and Intents.
583                    if (mHasPhone && mHasSms) {
584                        entry.intent = phoneIntent;
585                        entry.secondaryIntent = smsIntent;
586                        entry.secondaryActionIcon = kind.iconAltRes;
587                        entry.secondaryActionDescription = kind.iconAltDescriptionRes;
588                    } else if (mHasPhone) {
589                        entry.intent = phoneIntent;
590                    } else if (mHasSms) {
591                        entry.intent = smsIntent;
592                    } else {
593                        entry.intent = null;
594                    }
595
596                    // Remember super-primary phone
597                    if (isSuperPrimary) mPrimaryPhoneUri = entry.uri;
598
599                    entry.isPrimary = isSuperPrimary;
600
601                    // If the entry is a primary entry, then render it first in the view.
602                    if (entry.isPrimary) {
603                        // add to beginning of list so that this phone number shows up first
604                        mPhoneEntries.add(0, entry);
605                    } else {
606                        // add to end of list
607                        mPhoneEntries.add(entry);
608                    }
609                } else if (dataItem instanceof EmailDataItem && hasData) {
610                    // Build email entries
611                    entry.intent = new Intent(Intent.ACTION_SENDTO,
612                            Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
613                    entry.isPrimary = isSuperPrimary;
614                    // If entry is a primary entry, then render it first in the view.
615                    if (entry.isPrimary) {
616                        mEmailEntries.add(0, entry);
617                    } else {
618                        mEmailEntries.add(entry);
619                    }
620
621                    // When Email rows have status, create additional Im row
622                    final DataStatus status = mContactData.getStatuses().get(entry.id);
623                    if (status != null) {
624                        EmailDataItem email = (EmailDataItem) dataItem;
625                        ImDataItem im = ImDataItem.createFromEmail(email);
626
627                        final DetailViewEntry imEntry = DetailViewEntry.fromValues(mContext, im,
628                                mContactData.isDirectoryEntry(), mContactData.getDirectoryId());
629                        buildImActions(mContext, imEntry, im);
630                        imEntry.setPresence(status.getPresence());
631                        imEntry.maxLines = kind.maxLinesForDisplay;
632                        mImEntries.add(imEntry);
633                    }
634                } else if (dataItem instanceof StructuredPostalDataItem && hasData) {
635                    // Build postal entries
636                    entry.intent = StructuredPostalUtils.getViewPostalAddressIntent(entry.data);
637                    mPostalEntries.add(entry);
638                } else if (dataItem instanceof ImDataItem && hasData) {
639                    // Build IM entries
640                    buildImActions(mContext, entry, (ImDataItem) dataItem);
641
642                    // Apply presence when available
643                    final DataStatus status = mContactData.getStatuses().get(entry.id);
644                    if (status != null) {
645                        entry.setPresence(status.getPresence());
646                    }
647                    mImEntries.add(entry);
648                } else if (dataItem instanceof OrganizationDataItem) {
649                    // Organizations are not shown. The first one is shown in the header
650                    // and subsequent ones are not supported anymore
651                } else if (dataItem instanceof NicknameDataItem && hasData) {
652                    // Build nickname entries
653                    final boolean isNameRawContact =
654                        (mContactData.getNameRawContactId() == rawContactId);
655
656                    final boolean duplicatesTitle =
657                        isNameRawContact
658                        && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
659
660                    if (!duplicatesTitle) {
661                        entry.uri = null;
662                        mNicknameEntries.add(entry);
663                    }
664                } else if (dataItem instanceof NoteDataItem && hasData) {
665                    // Build note entries
666                    entry.uri = null;
667                    mNoteEntries.add(entry);
668                } else if (dataItem instanceof WebsiteDataItem && hasData) {
669                    // Build Website entries
670                    entry.uri = null;
671                    try {
672                        WebAddress webAddress = new WebAddress(entry.data);
673                        entry.intent = new Intent(Intent.ACTION_VIEW,
674                                Uri.parse(webAddress.toString()));
675                    } catch (ParseException e) {
676                        Log.e(TAG, "Couldn't parse website: " + entry.data);
677                    }
678                    mWebsiteEntries.add(entry);
679                } else if (dataItem instanceof SipAddressDataItem && hasData) {
680                    // Build SipAddress entries
681                    entry.uri = null;
682                    if (mHasSip) {
683                        entry.intent = ContactsUtils.getCallIntent(
684                                Uri.fromParts(Constants.SCHEME_SIP, entry.data, null));
685                    } else {
686                        entry.intent = null;
687                    }
688                    mSipEntries.add(entry);
689                    // TODO: Now that SipAddress is in its own list of entries
690                    // (instead of grouped in mOtherEntries), consider
691                    // repositioning it right under the phone number.
692                    // (Then, we'd also update FallbackAccountType.java to set
693                    // secondary=false for this field, and tweak the weight
694                    // of its DataKind.)
695                } else if (dataItem instanceof EventDataItem && hasData) {
696                    entry.data = DateUtils.formatDate(mContext, entry.data);
697                    entry.uri = null;
698                    mEventEntries.add(entry);
699                } else if (dataItem instanceof RelationDataItem && hasData) {
700                    entry.intent = new Intent(Intent.ACTION_SEARCH);
701                    entry.intent.putExtra(SearchManager.QUERY, entry.data);
702                    entry.intent.setType(Contacts.CONTENT_TYPE);
703                    mRelationEntries.add(entry);
704                } else {
705                    // Handle showing custom rows
706                    entry.intent = new Intent(Intent.ACTION_VIEW);
707                    entry.intent.setDataAndType(entry.uri, entry.mimetype);
708
709                    entry.data = dataItem.buildDataString();
710
711                    if (!TextUtils.isEmpty(entry.data)) {
712                        // If the account type exists in the hash map, add it as another entry for
713                        // that account type
714                        AccountType type = dataItem.getAccountType();
715                        if (mOtherEntriesMap.containsKey(type)) {
716                            List<DetailViewEntry> listEntries = mOtherEntriesMap.get(type);
717                            listEntries.add(entry);
718                        } else {
719                            // Otherwise create a new list with the entry and add it to the hash map
720                            List<DetailViewEntry> listEntries = new ArrayList<DetailViewEntry>();
721                            listEntries.add(entry);
722                            mOtherEntriesMap.put(type, listEntries);
723                        }
724                    }
725                }
726            }
727        }
728
729        if (!groups.isEmpty()) {
730            DetailViewEntry entry = new DetailViewEntry();
731            Collections.sort(groups);
732            StringBuilder sb = new StringBuilder();
733            int size = groups.size();
734            for (int i = 0; i < size; i++) {
735                if (i != 0) {
736                    sb.append(", ");
737                }
738                sb.append(groups.get(i));
739            }
740            entry.mimetype = GroupMembership.MIMETYPE;
741            entry.kind = mContext.getString(R.string.groupsLabel);
742            entry.data = sb.toString();
743            mGroupEntries.add(entry);
744        }
745    }
746
747    /**
748     * Collapse all contact detail entries into one aggregated list with a {@link HeaderViewEntry}
749     * at the top.
750     */
751    private void setupFlattenedList() {
752        // All contacts should have a header view (even if there is no data for the contact).
753        mAllEntries.add(new HeaderViewEntry());
754
755        addPhoneticName();
756
757        flattenList(mPhoneEntries);
758        flattenList(mSmsEntries);
759        flattenList(mEmailEntries);
760        flattenList(mImEntries);
761        flattenList(mNicknameEntries);
762        flattenList(mWebsiteEntries);
763
764        addNetworks();
765
766        flattenList(mSipEntries);
767        flattenList(mPostalEntries);
768        flattenList(mEventEntries);
769        flattenList(mGroupEntries);
770        flattenList(mRelationEntries);
771        flattenList(mNoteEntries);
772    }
773
774    /**
775     * Add phonetic name (if applicable) to the aggregated list of contact details. This has to be
776     * done manually because phonetic name doesn't have a mimetype or action intent.
777     */
778    private void addPhoneticName() {
779        String phoneticName = ContactDetailDisplayUtils.getPhoneticName(mContext, mContactData);
780        if (TextUtils.isEmpty(phoneticName)) {
781            return;
782        }
783
784        // Add a title
785        String phoneticNameKindTitle = mContext.getString(R.string.name_phonetic);
786        mAllEntries.add(new KindTitleViewEntry(phoneticNameKindTitle.toUpperCase()));
787
788        // Add the phonetic name
789        final DetailViewEntry entry = new DetailViewEntry();
790        entry.kind = phoneticNameKindTitle;
791        entry.data = phoneticName;
792        mAllEntries.add(entry);
793    }
794
795    /**
796     * Add attribution and other third-party entries (if applicable) under the "networks" section
797     * of the aggregated list of contact details. This has to be done manually because the
798     * attribution does not have a mimetype and the third-party entries don't have actually belong
799     * to the same {@link DataKind}.
800     */
801    private void addNetworks() {
802        String attribution = ContactDetailDisplayUtils.getAttribution(mContext, mContactData);
803        boolean hasAttribution = !TextUtils.isEmpty(attribution);
804        int networksCount = mOtherEntriesMap.keySet().size();
805
806        // Note: invitableCount will always be 0 for me profile.  (ContactLoader won't set
807        // invitable types for me profile.)
808        int invitableCount = mContactData.getInvitableAccountTypes().size();
809        if (!hasAttribution && networksCount == 0 && invitableCount == 0) {
810            return;
811        }
812
813        // Add a title
814        String networkKindTitle = mContext.getString(R.string.connections);
815        mAllEntries.add(new KindTitleViewEntry(networkKindTitle.toUpperCase()));
816
817        // Add the attribution if applicable
818        if (hasAttribution) {
819            final DetailViewEntry entry = new DetailViewEntry();
820            entry.kind = networkKindTitle;
821            entry.data = attribution;
822            mAllEntries.add(entry);
823
824            // Add a divider below the attribution if there are network details that will follow
825            if (networksCount > 0) {
826                mAllEntries.add(new SeparatorViewEntry());
827            }
828        }
829
830        // Add the other entries from third parties
831        for (AccountType accountType : mOtherEntriesMap.keySet()) {
832
833            // Add a title for each third party app
834            mAllEntries.add(new NetworkTitleViewEntry(mContext, accountType));
835
836            for (DetailViewEntry detailEntry : mOtherEntriesMap.get(accountType)) {
837                // Add indented separator
838                SeparatorViewEntry separatorEntry = new SeparatorViewEntry();
839                separatorEntry.setIsInSubSection(true);
840                mAllEntries.add(separatorEntry);
841
842                // Add indented detail
843                detailEntry.setIsInSubSection(true);
844                mAllEntries.add(detailEntry);
845            }
846        }
847
848        mOtherEntriesMap.clear();
849
850        // Add the "More networks" button, which opens the invitable account type list popup.
851        if (invitableCount > 0) {
852            addMoreNetworks();
853        }
854    }
855
856    /**
857     * Add the "More networks" entry.  When clicked, show a popup containing a list of invitable
858     * account types.
859     */
860    private void addMoreNetworks() {
861        // First, prepare for the popup.
862
863        // Adapter for the list popup.
864        final InvitableAccountTypesAdapter popupAdapter = new InvitableAccountTypesAdapter(mContext,
865                mContactData);
866
867        // Listener called when a popup item is clicked.
868        final AdapterView.OnItemClickListener popupItemListener
869                = new AdapterView.OnItemClickListener() {
870            @Override
871            public void onItemClick(AdapterView<?> parent, View view, int position,
872                    long id) {
873                if (mListener != null && mContactData != null) {
874                    mListener.onItemClicked(ContactsUtils.getInvitableIntent(
875                            popupAdapter.getItem(position) /* account type */,
876                            mContactData.getLookupUri()));
877                }
878            }
879        };
880
881        // Then create the click listener for the "More network" entry.  Open the popup.
882        View.OnClickListener onClickListener = new OnClickListener() {
883            @Override
884            public void onClick(View v) {
885                showListPopup(v, popupAdapter, popupItemListener);
886            }
887        };
888
889        // Finally create the entry.
890        mAllEntries.add(new AddConnectionViewEntry(mContext, onClickListener));
891    }
892
893    /**
894     * Iterate through {@link DetailViewEntry} in the given list and add it to a list of all
895     * entries. Add a {@link KindTitleViewEntry} at the start if the length of the list is not 0.
896     * Add {@link SeparatorViewEntry}s as dividers as appropriate. Clear the original list.
897     */
898    private void flattenList(ArrayList<DetailViewEntry> entries) {
899        int count = entries.size();
900
901        // Add a title for this kind by extracting the kind from the first entry
902        if (count > 0) {
903            String kind = entries.get(0).kind;
904            mAllEntries.add(new KindTitleViewEntry(kind.toUpperCase()));
905        }
906
907        // Add all the data entries for this kind
908        for (int i = 0; i < count; i++) {
909            // For all entries except the first one, add a divider above the entry
910            if (i != 0) {
911                mAllEntries.add(new SeparatorViewEntry());
912            }
913            mAllEntries.add(entries.get(i));
914        }
915
916        // Clear old list because it's not needed anymore.
917        entries.clear();
918    }
919
920    /**
921     * Maps group ID to the corresponding group name, collapses all synonymous groups.
922     * Ignores default groups (e.g. My Contacts) and favorites groups.
923     */
924    private void handleGroupMembership(
925            ArrayList<String> groups, List<GroupMetaData> groupMetaData, long groupId) {
926        if (groupMetaData == null) {
927            return;
928        }
929
930        for (GroupMetaData group : groupMetaData) {
931            if (group.getGroupId() == groupId) {
932                if (!group.isDefaultGroup() && !group.isFavorites()) {
933                    String title = group.getTitle();
934                    if (!TextUtils.isEmpty(title) && !groups.contains(title)) {
935                        groups.add(title);
936                    }
937                }
938                break;
939            }
940        }
941    }
942
943    /**
944     * Writes the Instant Messaging action into the given entry value.
945     */
946    @VisibleForTesting
947    public static void buildImActions(Context context, DetailViewEntry entry,
948            ImDataItem im) {
949        final boolean isEmail = im.isCreatedFromEmail();
950
951        if (!isEmail && !im.isProtocolValid()) {
952            return;
953        }
954
955        final String data = im.getData();
956        if (TextUtils.isEmpty(data)) {
957            return;
958        }
959
960        final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
961
962        if (protocol == Im.PROTOCOL_GOOGLE_TALK) {
963            final int chatCapability = im.getChatCapability();
964            entry.chatCapability = chatCapability;
965            entry.typeString = Im.getProtocolLabel(context.getResources(), Im.PROTOCOL_GOOGLE_TALK,
966                    null).toString();
967            if ((chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) {
968                entry.intent =
969                        new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
970                entry.secondaryIntent =
971                        new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call"));
972            } else if ((chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) {
973                // Allow Talking and Texting
974                entry.intent =
975                    new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
976                entry.secondaryIntent =
977                    new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call"));
978            } else {
979                entry.intent =
980                    new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
981            }
982        } else {
983            // Build an IM Intent
984            String host = im.getCustomProtocol();
985
986            if (protocol != Im.PROTOCOL_CUSTOM) {
987                // Try bringing in a well-known host for specific protocols
988                host = ContactsUtils.lookupProviderNameFromId(protocol);
989            }
990
991            if (!TextUtils.isEmpty(host)) {
992                final String authority = host.toLowerCase();
993                final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
994                        authority).appendPath(data).build();
995                entry.intent = new Intent(Intent.ACTION_SENDTO, imUri);
996            }
997        }
998    }
999
1000    /**
1001     * Show a list popup.  Used for "popup-able" entry, such as "More networks".
1002     */
1003    private void showListPopup(View anchorView, ListAdapter adapter,
1004            final AdapterView.OnItemClickListener onItemClickListener) {
1005        dismissPopupIfShown();
1006        mPopup = new ListPopupWindow(mContext, null);
1007        mPopup.setAnchorView(anchorView);
1008        mPopup.setWidth(anchorView.getWidth());
1009        mPopup.setAdapter(adapter);
1010        mPopup.setModal(true);
1011
1012        // We need to wrap the passed onItemClickListener here, so that we can dismiss() the
1013        // popup afterwards.  Otherwise we could directly use the passed listener.
1014        mPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1015            @Override
1016            public void onItemClick(AdapterView<?> parent, View view, int position,
1017                    long id) {
1018                onItemClickListener.onItemClick(parent, view, position, id);
1019                dismissPopupIfShown();
1020            }
1021        });
1022        mPopup.show();
1023    }
1024
1025    private void dismissPopupIfShown() {
1026        if (mPopup != null && mPopup.isShowing()) {
1027            mPopup.dismiss();
1028        }
1029        mPopup = null;
1030    }
1031
1032    /**
1033     * Base class for an item in the {@link ViewAdapter} list of data, which is
1034     * supplied to the {@link ListView}.
1035     */
1036    static class ViewEntry {
1037        private final int viewTypeForAdapter;
1038        protected long id = -1;
1039        /** Whether or not the entry can be focused on or not. */
1040        protected boolean isEnabled = false;
1041
1042        ViewEntry(int viewType) {
1043            viewTypeForAdapter = viewType;
1044        }
1045
1046        int getViewType() {
1047            return viewTypeForAdapter;
1048        }
1049
1050        long getId() {
1051            return id;
1052        }
1053
1054        boolean isEnabled(){
1055            return isEnabled;
1056        }
1057
1058        /**
1059         * Called when the entry is clicked.  Only {@link #isEnabled} entries can get clicked.
1060         *
1061         * @param clickedView  {@link View} that was clicked  (Used, for example, as the anchor view
1062         *        for a popup.)
1063         * @param fragmentListener  {@link Listener} set to {@link ContactDetailFragment}
1064         */
1065        public void click(View clickedView, Listener fragmentListener) {
1066        }
1067    }
1068
1069    /**
1070     * Header item in the {@link ViewAdapter} list of data.
1071     */
1072    private static class HeaderViewEntry extends ViewEntry {
1073
1074        HeaderViewEntry() {
1075            super(ViewAdapter.VIEW_TYPE_HEADER_ENTRY);
1076        }
1077
1078    }
1079
1080    /**
1081     * Separator between items of the same {@link DataKind} in the
1082     * {@link ViewAdapter} list of data.
1083     */
1084    private static class SeparatorViewEntry extends ViewEntry {
1085
1086        /**
1087         * Whether or not the entry is in a subsection (if true then the contents will be indented
1088         * to the right)
1089         */
1090        private boolean mIsInSubSection = false;
1091
1092        SeparatorViewEntry() {
1093            super(ViewAdapter.VIEW_TYPE_SEPARATOR_ENTRY);
1094        }
1095
1096        public void setIsInSubSection(boolean isInSubSection) {
1097            mIsInSubSection = isInSubSection;
1098        }
1099
1100        public boolean isInSubSection() {
1101            return mIsInSubSection;
1102        }
1103    }
1104
1105    /**
1106     * Title entry for items of the same {@link DataKind} in the
1107     * {@link ViewAdapter} list of data.
1108     */
1109    private static class KindTitleViewEntry extends ViewEntry {
1110
1111        private final String mTitle;
1112
1113        KindTitleViewEntry(String titleText) {
1114            super(ViewAdapter.VIEW_TYPE_KIND_TITLE_ENTRY);
1115            mTitle = titleText;
1116        }
1117
1118        public String getTitle() {
1119            return mTitle;
1120        }
1121    }
1122
1123    /**
1124     * A title for a section of contact details from a single 3rd party network.
1125     */
1126    private static class NetworkTitleViewEntry extends ViewEntry {
1127        private final Drawable mIcon;
1128        private final CharSequence mLabel;
1129
1130        public NetworkTitleViewEntry(Context context, AccountType type) {
1131            super(ViewAdapter.VIEW_TYPE_NETWORK_TITLE_ENTRY);
1132            this.mIcon = type.getDisplayIcon(context);
1133            this.mLabel = type.getDisplayLabel(context);
1134            this.isEnabled = false;
1135        }
1136
1137        public Drawable getIcon() {
1138            return mIcon;
1139        }
1140
1141        public CharSequence getLabel() {
1142            return mLabel;
1143        }
1144    }
1145
1146    /**
1147     * This is used for the "Add Connections" entry.
1148     */
1149    private static class AddConnectionViewEntry extends ViewEntry {
1150        private final Drawable mIcon;
1151        private final CharSequence mLabel;
1152        private final View.OnClickListener mOnClickListener;
1153
1154        private AddConnectionViewEntry(Context context, View.OnClickListener onClickListener) {
1155            super(ViewAdapter.VIEW_TYPE_ADD_CONNECTION_ENTRY);
1156            this.mIcon = context.getResources().getDrawable(
1157                    R.drawable.ic_menu_add_field_holo_light);
1158            this.mLabel = context.getString(R.string.add_connection_button);
1159            this.mOnClickListener = onClickListener;
1160            this.isEnabled = true;
1161        }
1162
1163        @Override
1164        public void click(View clickedView, Listener fragmentListener) {
1165            if (mOnClickListener == null) return;
1166            mOnClickListener.onClick(clickedView);
1167        }
1168
1169        public Drawable getIcon() {
1170            return mIcon;
1171        }
1172
1173        public CharSequence getLabel() {
1174            return mLabel;
1175        }
1176    }
1177
1178    /**
1179     * An item with a single detail for a contact in the {@link ViewAdapter}
1180     * list of data.
1181     */
1182    static class DetailViewEntry extends ViewEntry implements Collapsible<DetailViewEntry> {
1183        // TODO: Make getters/setters for these fields
1184        public int type = -1;
1185        public String kind;
1186        public String typeString;
1187        public String data;
1188        public Uri uri;
1189        public int maxLines = 1;
1190        public String mimetype;
1191
1192        public Context context = null;
1193        public boolean isPrimary = false;
1194        public int secondaryActionIcon = -1;
1195        public int secondaryActionDescription = -1;
1196        public Intent intent;
1197        public Intent secondaryIntent = null;
1198        public ArrayList<Long> ids = new ArrayList<Long>();
1199        public int collapseCount = 0;
1200
1201        public int presence = -1;
1202        public int chatCapability = 0;
1203
1204        private boolean mIsInSubSection = false;
1205
1206        @Override
1207        public String toString() {
1208            StringBuilder sb = new StringBuilder();
1209            sb.append("== DetailViewEntry ==\n");
1210            sb.append("  type: " + type + "\n");
1211            sb.append("  kind: " + kind + "\n");
1212            sb.append("  typeString: " + typeString + "\n");
1213            sb.append("  data: " + data + "\n");
1214            sb.append("  uri: " + uri.toString() + "\n");
1215            sb.append("  maxLines: " + maxLines + "\n");
1216            sb.append("  mimetype: " + mimetype + "\n");
1217            sb.append("  isPrimary: " + (isPrimary ? "true" : "false") + "\n");
1218            sb.append("  secondaryActionIcon: " + secondaryActionIcon + "\n");
1219            sb.append("  secondaryActionDescription: " + secondaryActionDescription + "\n");
1220            if (intent == null) {
1221                sb.append("  intent: " + intent.toString() + "\n");
1222            } else {
1223                sb.append("  intent: " + intent.toString() + "\n");
1224            }
1225            if (secondaryIntent == null) {
1226                sb.append("  secondaryIntent: (null)\n");
1227            } else {
1228                sb.append("  secondaryIntent: " + secondaryIntent.toString() + "\n");
1229            }
1230            sb.append("  ids: " + Iterables.toString(ids) + "\n");
1231            sb.append("  collapseCount: " + collapseCount + "\n");
1232            sb.append("  presence: " + presence + "\n");
1233            sb.append("  chatCapability: " + chatCapability + "\n");
1234            sb.append("  mIsInSubsection: " + (mIsInSubSection ? "true" : "false") + "\n");
1235            return sb.toString();
1236        }
1237
1238        DetailViewEntry() {
1239            super(ViewAdapter.VIEW_TYPE_DETAIL_ENTRY);
1240            isEnabled = true;
1241        }
1242
1243        /**
1244         * Build new {@link DetailViewEntry} and populate from the given values.
1245         */
1246        public static DetailViewEntry fromValues(Context context, DataItem item,
1247                boolean isDirectoryEntry, long directoryId) {
1248            final DetailViewEntry entry = new DetailViewEntry();
1249            entry.id = item.getId();
1250            entry.context = context;
1251            entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
1252            if (isDirectoryEntry) {
1253                entry.uri = entry.uri.buildUpon().appendQueryParameter(
1254                        ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
1255            }
1256            entry.mimetype = item.getMimeType();
1257            entry.kind = item.getKindString();
1258            entry.data = item.buildDataString();
1259
1260            if (item.hasKindTypeColumn()) {
1261                entry.type = item.getKindTypeColumn();
1262
1263                // get type string
1264                entry.typeString = "";
1265                for (EditType type : item.getDataKind().typeList) {
1266                    if (type.rawValue == entry.type) {
1267                        if (type.customColumn == null) {
1268                            // Non-custom type. Get its description from the resource
1269                            entry.typeString = context.getString(type.labelRes);
1270                        } else {
1271                            // Custom type. Read it from the database
1272                            entry.typeString =
1273                                    item.getContentValues().getAsString(type.customColumn);
1274                        }
1275                        break;
1276                    }
1277                }
1278            } else {
1279                entry.typeString = "";
1280            }
1281
1282            return entry;
1283        }
1284
1285        public void setPresence(int presence) {
1286            this.presence = presence;
1287        }
1288
1289        public void setIsInSubSection(boolean isInSubSection) {
1290            mIsInSubSection = isInSubSection;
1291        }
1292
1293        public boolean isInSubSection() {
1294            return mIsInSubSection;
1295        }
1296
1297        @Override
1298        public boolean collapseWith(DetailViewEntry entry) {
1299            // assert equal collapse keys
1300            if (!shouldCollapseWith(entry)) {
1301                return false;
1302            }
1303
1304            // Choose the label associated with the highest type precedence.
1305            if (TypePrecedence.getTypePrecedence(mimetype, type)
1306                    > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) {
1307                type = entry.type;
1308                kind = entry.kind;
1309                typeString = entry.typeString;
1310            }
1311
1312            // Choose the max of the maxLines and maxLabelLines values.
1313            maxLines = Math.max(maxLines, entry.maxLines);
1314
1315            // Choose the presence with the highest precedence.
1316            if (StatusUpdates.getPresencePrecedence(presence)
1317                    < StatusUpdates.getPresencePrecedence(entry.presence)) {
1318                presence = entry.presence;
1319            }
1320
1321            // If any of the collapsed entries are primary make the whole thing primary.
1322            isPrimary = entry.isPrimary ? true : isPrimary;
1323
1324            // uri, and contactdId, shouldn't make a difference. Just keep the original.
1325
1326            // Keep track of all the ids that have been collapsed with this one.
1327            ids.add(entry.getId());
1328            collapseCount++;
1329            return true;
1330        }
1331
1332        @Override
1333        public boolean shouldCollapseWith(DetailViewEntry entry) {
1334            if (entry == null) {
1335                return false;
1336            }
1337
1338            if (!ContactsUtils.shouldCollapse(mimetype, data, entry.mimetype, entry.data)) {
1339                return false;
1340            }
1341
1342            if (!TextUtils.equals(mimetype, entry.mimetype)
1343                    || !ContactsUtils.areIntentActionEqual(intent, entry.intent)
1344                    || !ContactsUtils.areIntentActionEqual(
1345                            secondaryIntent, entry.secondaryIntent)) {
1346                return false;
1347            }
1348
1349            return true;
1350        }
1351
1352        @Override
1353        public void click(View clickedView, Listener fragmentListener) {
1354            if (fragmentListener == null || intent == null) return;
1355            fragmentListener.onItemClicked(intent);
1356        }
1357    }
1358
1359    /**
1360     * Cache of the children views for a view that displays a header view entry.
1361     */
1362    private static class HeaderViewCache {
1363        public final TextView displayNameView;
1364        public final TextView companyView;
1365        public final ImageView photoView;
1366        public final View photoOverlayView;
1367        public final ImageView starredView;
1368        public final int layoutResourceId;
1369
1370        public HeaderViewCache(View view, int layoutResourceInflated) {
1371            displayNameView = (TextView) view.findViewById(R.id.name);
1372            companyView = (TextView) view.findViewById(R.id.company);
1373            photoView = (ImageView) view.findViewById(R.id.photo);
1374            photoOverlayView = view.findViewById(R.id.photo_touch_intercept_overlay);
1375            starredView = (ImageView) view.findViewById(R.id.star);
1376            layoutResourceId = layoutResourceInflated;
1377        }
1378
1379        public void enablePhotoOverlay(OnClickListener listener) {
1380            if (photoOverlayView != null) {
1381                photoOverlayView.setOnClickListener(listener);
1382                photoOverlayView.setVisibility(View.VISIBLE);
1383            }
1384        }
1385    }
1386
1387    private static class KindTitleViewCache {
1388        public final TextView titleView;
1389
1390        public KindTitleViewCache(View view) {
1391            titleView = (TextView)view.findViewById(R.id.title);
1392        }
1393    }
1394
1395    /**
1396     * Cache of the children views for a view that displays a {@link NetworkTitleViewEntry}
1397     */
1398    private static class NetworkTitleViewCache {
1399        public final TextView name;
1400        public final ImageView icon;
1401
1402        public NetworkTitleViewCache(View view) {
1403            name = (TextView) view.findViewById(R.id.network_title);
1404            icon = (ImageView) view.findViewById(R.id.network_icon);
1405        }
1406    }
1407
1408    /**
1409     * Cache of the children views for a view that displays a {@link AddConnectionViewEntry}
1410     */
1411    private static class AddConnectionViewCache {
1412        public final TextView name;
1413        public final ImageView icon;
1414        public final View primaryActionView;
1415
1416        public AddConnectionViewCache(View view) {
1417            name = (TextView) view.findViewById(R.id.add_connection_label);
1418            icon = (ImageView) view.findViewById(R.id.add_connection_icon);
1419            primaryActionView = view.findViewById(R.id.primary_action_view);
1420        }
1421    }
1422
1423    /**
1424     * Cache of the children views of a contact detail entry represented by a
1425     * {@link DetailViewEntry}
1426     */
1427    private static class DetailViewCache {
1428        public final TextView type;
1429        public final TextView data;
1430        public final ImageView presenceIcon;
1431        public final ImageView secondaryActionButton;
1432        public final View actionsViewContainer;
1433        public final View primaryActionView;
1434        public final View secondaryActionViewContainer;
1435        public final View secondaryActionDivider;
1436        public final View primaryIndicator;
1437
1438        public DetailViewCache(View view,
1439                OnClickListener primaryActionClickListener,
1440                OnClickListener secondaryActionClickListener) {
1441            type = (TextView) view.findViewById(R.id.type);
1442            data = (TextView) view.findViewById(R.id.data);
1443            primaryIndicator = view.findViewById(R.id.primary_indicator);
1444            presenceIcon = (ImageView) view.findViewById(R.id.presence_icon);
1445
1446            actionsViewContainer = view.findViewById(R.id.actions_view_container);
1447            actionsViewContainer.setOnClickListener(primaryActionClickListener);
1448            primaryActionView = view.findViewById(R.id.primary_action_view);
1449
1450            secondaryActionViewContainer = view.findViewById(
1451                    R.id.secondary_action_view_container);
1452            secondaryActionViewContainer.setOnClickListener(
1453                    secondaryActionClickListener);
1454            secondaryActionButton = (ImageView) view.findViewById(
1455                    R.id.secondary_action_button);
1456
1457            secondaryActionDivider = view.findViewById(R.id.vertical_divider);
1458        }
1459    }
1460
1461    private final class ViewAdapter extends BaseAdapter {
1462
1463        public static final int VIEW_TYPE_DETAIL_ENTRY = 0;
1464        public static final int VIEW_TYPE_HEADER_ENTRY = 1;
1465        public static final int VIEW_TYPE_KIND_TITLE_ENTRY = 2;
1466        public static final int VIEW_TYPE_NETWORK_TITLE_ENTRY = 3;
1467        public static final int VIEW_TYPE_ADD_CONNECTION_ENTRY = 4;
1468        public static final int VIEW_TYPE_SEPARATOR_ENTRY = 5;
1469        private static final int VIEW_TYPE_COUNT = 6;
1470
1471        @Override
1472        public View getView(int position, View convertView, ViewGroup parent) {
1473            switch (getItemViewType(position)) {
1474                case VIEW_TYPE_HEADER_ENTRY:
1475                    return getHeaderEntryView(convertView, parent);
1476                case VIEW_TYPE_SEPARATOR_ENTRY:
1477                    return getSeparatorEntryView(position, convertView, parent);
1478                case VIEW_TYPE_KIND_TITLE_ENTRY:
1479                    return getKindTitleEntryView(position, convertView, parent);
1480                case VIEW_TYPE_DETAIL_ENTRY:
1481                    return getDetailEntryView(position, convertView, parent);
1482                case VIEW_TYPE_NETWORK_TITLE_ENTRY:
1483                    return getNetworkTitleEntryView(position, convertView, parent);
1484                case VIEW_TYPE_ADD_CONNECTION_ENTRY:
1485                    return getAddConnectionEntryView(position, convertView, parent);
1486                default:
1487                    throw new IllegalStateException("Invalid view type ID " +
1488                            getItemViewType(position));
1489            }
1490        }
1491
1492        private View getHeaderEntryView(View convertView, ViewGroup parent) {
1493            final int desiredLayoutResourceId = mContactHasSocialUpdates ?
1494                    R.layout.detail_header_contact_with_updates :
1495                    R.layout.detail_header_contact_without_updates;
1496            View result = null;
1497            HeaderViewCache viewCache = null;
1498
1499            // Only use convertView if it has the same layout resource ID as the one desired
1500            // (the two can be different on wide 2-pane screens where the detail fragment is reused
1501            // for many different contacts that do and do not have social updates).
1502            if (convertView != null) {
1503                viewCache = (HeaderViewCache) convertView.getTag();
1504                if (viewCache.layoutResourceId == desiredLayoutResourceId) {
1505                    result = convertView;
1506                }
1507            }
1508
1509            // Otherwise inflate a new header view and create a new view cache.
1510            if (result == null) {
1511                result = mInflater.inflate(desiredLayoutResourceId, parent, false);
1512                viewCache = new HeaderViewCache(result, desiredLayoutResourceId);
1513                result.setTag(viewCache);
1514            }
1515
1516            ContactDetailDisplayUtils.setDisplayName(mContext, mContactData,
1517                    viewCache.displayNameView);
1518            ContactDetailDisplayUtils.setCompanyName(mContext, mContactData, viewCache.companyView);
1519
1520            // Set the photo if it should be displayed
1521            if (viewCache.photoView != null) {
1522                final boolean expandOnClick = mContactData.getPhotoUri() != null;
1523                final OnClickListener listener = mPhotoSetter.setupContactPhotoForClick(
1524                        mContext, mContactData, viewCache.photoView, expandOnClick);
1525
1526                if (expandOnClick || mContactData.isWritableContact(mContext)) {
1527                    viewCache.enablePhotoOverlay(listener);
1528                }
1529            }
1530
1531            // Set the starred state if it should be displayed
1532            final ImageView favoritesStar = viewCache.starredView;
1533            if (favoritesStar != null) {
1534                ContactDetailDisplayUtils.configureStarredImageView(favoritesStar,
1535                        mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
1536                        mContactData.getStarred());
1537                final Uri lookupUri = mContactData.getLookupUri();
1538                favoritesStar.setOnClickListener(new OnClickListener() {
1539                    @Override
1540                    public void onClick(View v) {
1541                        // Toggle "starred" state
1542                        // Make sure there is a contact
1543                        if (lookupUri != null) {
1544                            // Read the current starred value from the UI instead of using the last
1545                            // loaded state. This allows rapid tapping without writing the same
1546                            // value several times
1547                            final Object tag = favoritesStar.getTag();
1548                            final boolean isStarred = tag == null
1549                                    ? false : (Boolean) favoritesStar.getTag();
1550
1551                            // To improve responsiveness, swap out the picture (and tag) in the UI
1552                            // already
1553                            ContactDetailDisplayUtils.configureStarredImageView(favoritesStar,
1554                                    mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
1555                                    !isStarred);
1556
1557                            // Now perform the real save
1558                            Intent intent = ContactSaveService.createSetStarredIntent(
1559                                    getContext(), lookupUri, !isStarred);
1560                            getContext().startService(intent);
1561                        }
1562                    }
1563                });
1564            }
1565
1566            return result;
1567        }
1568
1569        private View getSeparatorEntryView(int position, View convertView, ViewGroup parent) {
1570            final SeparatorViewEntry entry = (SeparatorViewEntry) getItem(position);
1571            final View result = (convertView != null) ? convertView :
1572                    mInflater.inflate(R.layout.contact_detail_separator_entry_view, parent, false);
1573
1574            result.setPadding(entry.isInSubSection() ? mViewEntryDimensions.getWidePaddingLeft() :
1575                    mViewEntryDimensions.getPaddingLeft(), 0,
1576                    mViewEntryDimensions.getPaddingRight(), 0);
1577
1578            return result;
1579        }
1580
1581        private View getKindTitleEntryView(int position, View convertView, ViewGroup parent) {
1582            final KindTitleViewEntry entry = (KindTitleViewEntry) getItem(position);
1583            final View result;
1584            final KindTitleViewCache viewCache;
1585
1586            if (convertView != null) {
1587                result = convertView;
1588                viewCache = (KindTitleViewCache)result.getTag();
1589            } else {
1590                result = mInflater.inflate(R.layout.list_separator, parent, false);
1591                viewCache = new KindTitleViewCache(result);
1592                result.setTag(viewCache);
1593            }
1594
1595            viewCache.titleView.setText(entry.getTitle());
1596
1597            return result;
1598        }
1599
1600        private View getNetworkTitleEntryView(int position, View convertView, ViewGroup parent) {
1601            final NetworkTitleViewEntry entry = (NetworkTitleViewEntry) getItem(position);
1602            final View result;
1603            final NetworkTitleViewCache viewCache;
1604
1605            if (convertView != null) {
1606                result = convertView;
1607                viewCache = (NetworkTitleViewCache) result.getTag();
1608            } else {
1609                result = mInflater.inflate(R.layout.contact_detail_network_title_entry_view,
1610                        parent, false);
1611                viewCache = new NetworkTitleViewCache(result);
1612                result.setTag(viewCache);
1613            }
1614
1615            viewCache.name.setText(entry.getLabel());
1616            viewCache.icon.setImageDrawable(entry.getIcon());
1617
1618            return result;
1619        }
1620
1621        private View getAddConnectionEntryView(int position, View convertView, ViewGroup parent) {
1622            final AddConnectionViewEntry entry = (AddConnectionViewEntry) getItem(position);
1623            final View result;
1624            final AddConnectionViewCache viewCache;
1625
1626            if (convertView != null) {
1627                result = convertView;
1628                viewCache = (AddConnectionViewCache) result.getTag();
1629            } else {
1630                result = mInflater.inflate(R.layout.contact_detail_add_connection_entry_view,
1631                        parent, false);
1632                viewCache = new AddConnectionViewCache(result);
1633                result.setTag(viewCache);
1634            }
1635            viewCache.name.setText(entry.getLabel());
1636            viewCache.icon.setImageDrawable(entry.getIcon());
1637            viewCache.primaryActionView.setOnClickListener(entry.mOnClickListener);
1638
1639            return result;
1640        }
1641
1642        private View getDetailEntryView(int position, View convertView, ViewGroup parent) {
1643            final DetailViewEntry entry = (DetailViewEntry) getItem(position);
1644            final View v;
1645            final DetailViewCache viewCache;
1646
1647            // Check to see if we can reuse convertView
1648            if (convertView != null) {
1649                v = convertView;
1650                viewCache = (DetailViewCache) v.getTag();
1651            } else {
1652                // Create a new view if needed
1653                v = mInflater.inflate(R.layout.contact_detail_list_item, parent, false);
1654
1655                // Cache the children
1656                viewCache = new DetailViewCache(v,
1657                        mPrimaryActionClickListener, mSecondaryActionClickListener);
1658                v.setTag(viewCache);
1659            }
1660
1661            bindDetailView(position, v, entry);
1662            return v;
1663        }
1664
1665        private void bindDetailView(int position, View view, DetailViewEntry entry) {
1666            final Resources resources = mContext.getResources();
1667            DetailViewCache views = (DetailViewCache) view.getTag();
1668
1669            if (!TextUtils.isEmpty(entry.typeString)) {
1670                views.type.setText(entry.typeString.toUpperCase());
1671                views.type.setVisibility(View.VISIBLE);
1672            } else {
1673                views.type.setVisibility(View.GONE);
1674            }
1675
1676            views.data.setText(entry.data);
1677            setMaxLines(views.data, entry.maxLines);
1678
1679            // Set the default contact method
1680            views.primaryIndicator.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
1681
1682            // Set the presence icon
1683            final Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
1684                    mContext, entry.presence);
1685            final ImageView presenceIconView = views.presenceIcon;
1686            if (presenceIcon != null) {
1687                presenceIconView.setImageDrawable(presenceIcon);
1688                presenceIconView.setVisibility(View.VISIBLE);
1689            } else {
1690                presenceIconView.setVisibility(View.GONE);
1691            }
1692
1693            final ActionsViewContainer actionsButtonContainer =
1694                    (ActionsViewContainer) views.actionsViewContainer;
1695            actionsButtonContainer.setTag(entry);
1696            actionsButtonContainer.setPosition(position);
1697            registerForContextMenu(actionsButtonContainer);
1698
1699            // Set the secondary action button
1700            final ImageView secondaryActionView = views.secondaryActionButton;
1701            Drawable secondaryActionIcon = null;
1702            String secondaryActionDescription = null;
1703            if (entry.secondaryActionIcon != -1) {
1704                secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
1705                secondaryActionDescription = resources.getString(entry.secondaryActionDescription);
1706            } else if ((entry.chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) {
1707                secondaryActionIcon =
1708                        resources.getDrawable(R.drawable.sym_action_videochat_holo_light);
1709                secondaryActionDescription = resources.getString(R.string.video_chat);
1710            } else if ((entry.chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) {
1711                secondaryActionIcon =
1712                        resources.getDrawable(R.drawable.sym_action_audiochat_holo_light);
1713                secondaryActionDescription = resources.getString(R.string.audio_chat);
1714            }
1715
1716            final View secondaryActionViewContainer = views.secondaryActionViewContainer;
1717            if (entry.secondaryIntent != null && secondaryActionIcon != null) {
1718                secondaryActionView.setImageDrawable(secondaryActionIcon);
1719                secondaryActionView.setContentDescription(secondaryActionDescription);
1720                secondaryActionViewContainer.setTag(entry);
1721                secondaryActionViewContainer.setVisibility(View.VISIBLE);
1722                views.secondaryActionDivider.setVisibility(View.VISIBLE);
1723            } else {
1724                secondaryActionViewContainer.setVisibility(View.GONE);
1725                views.secondaryActionDivider.setVisibility(View.GONE);
1726            }
1727
1728            // Right and left padding should not have "pressed" effect.
1729            view.setPadding(
1730                    entry.isInSubSection()
1731                            ? mViewEntryDimensions.getWidePaddingLeft()
1732                            : mViewEntryDimensions.getPaddingLeft(),
1733                    0, mViewEntryDimensions.getPaddingRight(), 0);
1734            // Top and bottom padding should have "pressed" effect.
1735            final View primaryActionView = views.primaryActionView;
1736            primaryActionView.setPadding(
1737                    primaryActionView.getPaddingLeft(),
1738                    mViewEntryDimensions.getPaddingTop(),
1739                    primaryActionView.getPaddingRight(),
1740                    mViewEntryDimensions.getPaddingBottom());
1741            secondaryActionViewContainer.setPadding(
1742                    secondaryActionViewContainer.getPaddingLeft(),
1743                    mViewEntryDimensions.getPaddingTop(),
1744                    secondaryActionViewContainer.getPaddingRight(),
1745                    mViewEntryDimensions.getPaddingBottom());
1746        }
1747
1748        private void setMaxLines(TextView textView, int maxLines) {
1749            if (maxLines == 1) {
1750                textView.setSingleLine(true);
1751                textView.setEllipsize(TextUtils.TruncateAt.END);
1752            } else {
1753                textView.setSingleLine(false);
1754                textView.setMaxLines(maxLines);
1755                textView.setEllipsize(null);
1756            }
1757        }
1758
1759        private final OnClickListener mPrimaryActionClickListener = new OnClickListener() {
1760            @Override
1761            public void onClick(View view) {
1762                if (mListener == null) return;
1763                final ViewEntry entry = (ViewEntry) view.getTag();
1764                if (entry == null) return;
1765                entry.click(view, mListener);
1766            }
1767        };
1768
1769        private final OnClickListener mSecondaryActionClickListener = new OnClickListener() {
1770            @Override
1771            public void onClick(View view) {
1772                if (mListener == null) return;
1773                if (view == null) return;
1774                final ViewEntry entry = (ViewEntry) view.getTag();
1775                if (entry == null || !(entry instanceof DetailViewEntry)) return;
1776                final DetailViewEntry detailViewEntry = (DetailViewEntry) entry;
1777                final Intent intent = detailViewEntry.secondaryIntent;
1778                if (intent == null) return;
1779                mListener.onItemClicked(intent);
1780            }
1781        };
1782
1783        @Override
1784        public int getCount() {
1785            return mAllEntries.size();
1786        }
1787
1788        @Override
1789        public ViewEntry getItem(int position) {
1790            return mAllEntries.get(position);
1791        }
1792
1793        @Override
1794        public int getItemViewType(int position) {
1795            return mAllEntries.get(position).getViewType();
1796        }
1797
1798        @Override
1799        public int getViewTypeCount() {
1800            return VIEW_TYPE_COUNT;
1801        }
1802
1803        @Override
1804        public long getItemId(int position) {
1805            final ViewEntry entry = mAllEntries.get(position);
1806            if (entry != null) {
1807                return entry.getId();
1808            }
1809            return -1;
1810        }
1811
1812        @Override
1813        public boolean areAllItemsEnabled() {
1814            // Header will always be an item that is not enabled.
1815            return false;
1816        }
1817
1818        @Override
1819        public boolean isEnabled(int position) {
1820            return getItem(position).isEnabled();
1821        }
1822    }
1823
1824    @Override
1825    public void onAccountSelectorCancelled() {
1826    }
1827
1828    @Override
1829    public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) {
1830        createCopy(account);
1831    }
1832
1833    private void createCopy(AccountWithDataSet account) {
1834        if (mListener != null) {
1835            mListener.onCreateRawContactRequested(mContactData.getContentValues(), account);
1836        }
1837    }
1838
1839    /**
1840     * Default (fallback) list item click listener.  Note the click event for DetailViewEntry is
1841     * caught by individual views in the list item view to distinguish the primary action and the
1842     * secondary action, so this method won't be invoked for that.  (The listener is set in the
1843     * bindview in the adapter)
1844     * This listener is used for other kind of entries.
1845     */
1846    @Override
1847    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1848        if (mListener == null) return;
1849        final ViewEntry entry = mAdapter.getItem(position);
1850        if (entry == null) return;
1851        entry.click(view, mListener);
1852    }
1853
1854    @Override
1855    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
1856        super.onCreateContextMenu(menu, view, menuInfo);
1857
1858        AdapterView.AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
1859        DetailViewEntry selectedEntry = (DetailViewEntry) mAllEntries.get(info.position);
1860
1861        menu.setHeaderTitle(selectedEntry.data);
1862        menu.add(ContextMenu.NONE, ContextMenuIds.COPY_TEXT,
1863                ContextMenu.NONE, getString(R.string.copy_text));
1864
1865        String selectedMimeType = selectedEntry.mimetype;
1866
1867        // Defaults to true will only enable the detail to be copied to the clipboard.
1868        boolean isUniqueMimeType = true;
1869
1870        // Only allow primary support for Phone and Email content types
1871        if (Phone.CONTENT_ITEM_TYPE.equals(selectedMimeType)) {
1872            isUniqueMimeType = mIsUniqueNumber;
1873        } else if (Email.CONTENT_ITEM_TYPE.equals(selectedMimeType)) {
1874            isUniqueMimeType = mIsUniqueEmail;
1875        }
1876
1877        // Checking for previously set default
1878        if (selectedEntry.isPrimary) {
1879            menu.add(ContextMenu.NONE, ContextMenuIds.CLEAR_DEFAULT,
1880                    ContextMenu.NONE, getString(R.string.clear_default));
1881        } else if (!isUniqueMimeType) {
1882            menu.add(ContextMenu.NONE, ContextMenuIds.SET_DEFAULT,
1883                    ContextMenu.NONE, getString(R.string.set_default));
1884        }
1885    }
1886
1887    @Override
1888    public boolean onContextItemSelected(MenuItem item) {
1889        AdapterView.AdapterContextMenuInfo menuInfo;
1890        try {
1891            menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
1892        } catch (ClassCastException e) {
1893            Log.e(TAG, "bad menuInfo", e);
1894            return false;
1895        }
1896
1897        switch (item.getItemId()) {
1898            case ContextMenuIds.COPY_TEXT:
1899                copyToClipboard(menuInfo.position);
1900                return true;
1901            case ContextMenuIds.SET_DEFAULT:
1902                setDefaultContactMethod(mListView.getItemIdAtPosition(menuInfo.position));
1903                return true;
1904            case ContextMenuIds.CLEAR_DEFAULT:
1905                clearDefaultContactMethod(mListView.getItemIdAtPosition(menuInfo.position));
1906                return true;
1907            default:
1908                throw new IllegalArgumentException("Unknown menu option " + item.getItemId());
1909        }
1910    }
1911
1912    private void setDefaultContactMethod(long id) {
1913        Intent setIntent = ContactSaveService.createSetSuperPrimaryIntent(mContext, id);
1914        mContext.startService(setIntent);
1915    }
1916
1917    private void clearDefaultContactMethod(long id) {
1918        Intent clearIntent = ContactSaveService.createClearPrimaryIntent(mContext, id);
1919        mContext.startService(clearIntent);
1920    }
1921
1922    private void copyToClipboard(int viewEntryPosition) {
1923        // Getting the text to copied
1924        DetailViewEntry detailViewEntry = (DetailViewEntry) mAllEntries.get(viewEntryPosition);
1925        CharSequence textToCopy = detailViewEntry.data;
1926
1927        // Checking for empty string
1928        if (TextUtils.isEmpty(textToCopy)) return;
1929
1930        ClipboardUtils.copyText(getActivity(), detailViewEntry.typeString, textToCopy, true);
1931    }
1932
1933    @Override
1934    public boolean handleKeyDown(int keyCode) {
1935        switch (keyCode) {
1936            case KeyEvent.KEYCODE_CALL: {
1937                try {
1938                    ITelephony phone = ITelephony.Stub.asInterface(
1939                            ServiceManager.checkService("phone"));
1940                    if (phone != null && !phone.isIdle()) {
1941                        // Skip out and let the key be handled at a higher level
1942                        break;
1943                    }
1944                } catch (RemoteException re) {
1945                    // Fall through and try to call the contact
1946                }
1947
1948                int index = mListView.getSelectedItemPosition();
1949                if (index != -1) {
1950                    final DetailViewEntry entry = (DetailViewEntry) mAdapter.getItem(index);
1951                    if (entry != null && entry.intent != null &&
1952                            entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) {
1953                        mContext.startActivity(entry.intent);
1954                        return true;
1955                    }
1956                } else if (mPrimaryPhoneUri != null) {
1957                    // There isn't anything selected, call the default number
1958                    mContext.startActivity(ContactsUtils.getCallIntent(mPrimaryPhoneUri));
1959                    return true;
1960                }
1961                return false;
1962            }
1963        }
1964
1965        return false;
1966    }
1967
1968    /**
1969     * Base class for QuickFixes. QuickFixes quickly fix issues with the Contact without
1970     * requiring the user to go to the editor. Example: Add to My Contacts.
1971     */
1972    private static abstract class QuickFix {
1973        public abstract boolean isApplicable();
1974        public abstract String getTitle();
1975        public abstract void execute();
1976    }
1977
1978    private class AddToMyContactsQuickFix extends QuickFix {
1979        @Override
1980        public boolean isApplicable() {
1981            // Only local contacts
1982            if (mContactData == null || mContactData.isDirectoryEntry()) return false;
1983
1984            // User profile cannot be added to contacts
1985            if (mContactData.isUserProfile()) return false;
1986
1987            // Only if exactly one raw contact
1988            if (mContactData.getRawContacts().size() != 1) return false;
1989
1990            // test if the default group is assigned
1991            final List<GroupMetaData> groups = mContactData.getGroupMetaData();
1992
1993            // For accounts without group support, groups is null
1994            if (groups == null) return false;
1995
1996            // remember the default group id. no default group? bail out early
1997            final long defaultGroupId = getDefaultGroupId(groups);
1998            if (defaultGroupId == -1) return false;
1999
2000            final RawContact rawContact = (RawContact) mContactData.getRawContacts().get(0);
2001            final AccountType type = rawContact.getAccountType();
2002            // Offline or non-writeable account? Nothing to fix
2003            if (type == null || !type.areContactsWritable()) return false;
2004
2005            // Check whether the contact is in the default group
2006            boolean isInDefaultGroup = false;
2007            for (DataItem dataItem : Iterables.filter(
2008                    rawContact.getDataItems(), GroupMembershipDataItem.class)) {
2009                GroupMembershipDataItem groupMembership = (GroupMembershipDataItem) dataItem;
2010                final Long groupId = groupMembership.getGroupRowId();
2011                if (groupId == defaultGroupId) {
2012                    isInDefaultGroup = true;
2013                    break;
2014                }
2015            }
2016
2017            return !isInDefaultGroup;
2018        }
2019
2020        @Override
2021        public String getTitle() {
2022            return getString(R.string.add_to_my_contacts);
2023        }
2024
2025        @Override
2026        public void execute() {
2027            final long defaultGroupId = getDefaultGroupId(mContactData.getGroupMetaData());
2028            // there should always be a default group (otherwise the button would be invisible),
2029            // but let's be safe here
2030            if (defaultGroupId == -1) return;
2031
2032            // add the group membership to the current state
2033            final RawContactDeltaList contactDeltaList = mContactData.createRawContactDeltaList();
2034            final RawContactDelta rawContactEntityDelta = contactDeltaList.get(0);
2035
2036            final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
2037            final AccountType type = rawContactEntityDelta.getAccountType(accountTypes);
2038            final DataKind groupMembershipKind = type.getKindForMimetype(
2039                    GroupMembership.CONTENT_ITEM_TYPE);
2040            final ValuesDelta entry = RawContactModifier.insertChild(rawContactEntityDelta,
2041                    groupMembershipKind);
2042            entry.setGroupRowId(defaultGroupId);
2043
2044            // and fire off the intent. we don't need a callback, as the database listener
2045            // should update the ui
2046            final Intent intent = ContactSaveService.createSaveContactIntent(getActivity(),
2047                    contactDeltaList, "", 0, false, getActivity().getClass(),
2048                    Intent.ACTION_VIEW, null);
2049            getActivity().startService(intent);
2050        }
2051    }
2052
2053    private class MakeLocalCopyQuickFix extends QuickFix {
2054        @Override
2055        public boolean isApplicable() {
2056            // Not a directory contact? Nothing to fix here
2057            if (mContactData == null || !mContactData.isDirectoryEntry()) return false;
2058
2059            // No export support? Too bad
2060            if (mContactData.getDirectoryExportSupport() == Directory.EXPORT_SUPPORT_NONE) {
2061                return false;
2062            }
2063
2064            return true;
2065        }
2066
2067        @Override
2068        public String getTitle() {
2069            return getString(R.string.menu_copyContact);
2070        }
2071
2072        @Override
2073        public void execute() {
2074            if (mListener == null) {
2075                return;
2076            }
2077
2078            int exportSupport = mContactData.getDirectoryExportSupport();
2079            switch (exportSupport) {
2080                case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY: {
2081                    createCopy(new AccountWithDataSet(mContactData.getDirectoryAccountName(),
2082                                    mContactData.getDirectoryAccountType(), null));
2083                    break;
2084                }
2085                case Directory.EXPORT_SUPPORT_ANY_ACCOUNT: {
2086                    final List<AccountWithDataSet> accounts =
2087                            AccountTypeManager.getInstance(mContext).getAccounts(true);
2088                    if (accounts.isEmpty()) {
2089                        createCopy(null);
2090                        return;  // Don't show a dialog.
2091                    }
2092
2093                    // In the common case of a single writable account, auto-select
2094                    // it without showing a dialog.
2095                    if (accounts.size() == 1) {
2096                        createCopy(accounts.get(0));
2097                        return;  // Don't show a dialog.
2098                    }
2099
2100                    SelectAccountDialogFragment.show(getFragmentManager(),
2101                            ContactDetailFragment.this, R.string.dialog_new_contact_account,
2102                            AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, null);
2103                    break;
2104                }
2105            }
2106        }
2107    }
2108
2109    /**
2110     * This class loads the correct padding values for a contact detail item so they can be applied
2111     * dynamically. For example, this supports the case where some detail items can be indented and
2112     * need extra padding.
2113     */
2114    private static class ViewEntryDimensions {
2115
2116        private final int mWidePaddingLeft;
2117        private final int mPaddingLeft;
2118        private final int mPaddingRight;
2119        private final int mPaddingTop;
2120        private final int mPaddingBottom;
2121
2122        public ViewEntryDimensions(Resources resources) {
2123            mPaddingLeft = resources.getDimensionPixelSize(
2124                    R.dimen.detail_item_side_margin);
2125            mPaddingTop = resources.getDimensionPixelSize(
2126                    R.dimen.detail_item_vertical_margin);
2127            mWidePaddingLeft = mPaddingLeft +
2128                    resources.getDimensionPixelSize(R.dimen.detail_item_icon_margin) +
2129                    resources.getDimensionPixelSize(R.dimen.detail_network_icon_size);
2130            mPaddingRight = mPaddingLeft;
2131            mPaddingBottom = mPaddingTop;
2132        }
2133
2134        public int getWidePaddingLeft() {
2135            return mWidePaddingLeft;
2136        }
2137
2138        public int getPaddingLeft() {
2139            return mPaddingLeft;
2140        }
2141
2142        public int getPaddingRight() {
2143            return mPaddingRight;
2144        }
2145
2146        public int getPaddingTop() {
2147            return mPaddingTop;
2148        }
2149
2150        public int getPaddingBottom() {
2151            return mPaddingBottom;
2152        }
2153    }
2154
2155    public static interface Listener {
2156        /**
2157         * User clicked a single item (e.g. mail). The intent passed in could be null.
2158         */
2159        public void onItemClicked(Intent intent);
2160
2161        /**
2162         * User requested creation of a new contact with the specified values.
2163         *
2164         * @param values ContentValues containing data rows for the new contact.
2165         * @param account Account where the new contact should be created.
2166         */
2167        public void onCreateRawContactRequested(ArrayList<ContentValues> values,
2168                AccountWithDataSet account);
2169    }
2170
2171    /**
2172     * Adapter for the invitable account types; used for the invitable account type list popup.
2173     */
2174    private final static class InvitableAccountTypesAdapter extends BaseAdapter {
2175        private final Context mContext;
2176        private final LayoutInflater mInflater;
2177        private final ArrayList<AccountType> mAccountTypes;
2178
2179        public InvitableAccountTypesAdapter(Context context, Contact contactData) {
2180            mContext = context;
2181            mInflater = LayoutInflater.from(context);
2182            final List<AccountType> types = contactData.getInvitableAccountTypes();
2183            mAccountTypes = new ArrayList<AccountType>(types.size());
2184
2185            for (int i = 0; i < types.size(); i++) {
2186                mAccountTypes.add(types.get(i));
2187            }
2188
2189            Collections.sort(mAccountTypes, new AccountType.DisplayLabelComparator(mContext));
2190        }
2191
2192        @Override
2193        public View getView(int position, View convertView, ViewGroup parent) {
2194            final View resultView =
2195                    (convertView != null) ? convertView
2196                    : mInflater.inflate(R.layout.account_selector_list_item, parent, false);
2197
2198            final TextView text1 = (TextView)resultView.findViewById(android.R.id.text1);
2199            final TextView text2 = (TextView)resultView.findViewById(android.R.id.text2);
2200            final ImageView icon = (ImageView)resultView.findViewById(android.R.id.icon);
2201
2202            final AccountType accountType = mAccountTypes.get(position);
2203
2204            CharSequence action = accountType.getInviteContactActionLabel(mContext);
2205            CharSequence label = accountType.getDisplayLabel(mContext);
2206            if (TextUtils.isEmpty(action)) {
2207                text1.setText(label);
2208                text2.setVisibility(View.GONE);
2209            } else {
2210                text1.setText(action);
2211                text2.setVisibility(View.VISIBLE);
2212                text2.setText(label);
2213            }
2214            icon.setImageDrawable(accountType.getDisplayIcon(mContext));
2215
2216            return resultView;
2217        }
2218
2219        @Override
2220        public int getCount() {
2221            return mAccountTypes.size();
2222        }
2223
2224        @Override
2225        public AccountType getItem(int position) {
2226            return mAccountTypes.get(position);
2227        }
2228
2229        @Override
2230        public long getItemId(int position) {
2231            return position;
2232        }
2233    }
2234}
2235