Home | History | Annotate | Download | only in detail
      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 
     17 package com.android.contacts.detail;
     18 
     19 import android.app.Activity;
     20 import android.app.Fragment;
     21 import android.app.SearchManager;
     22 import android.content.ContentUris;
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.res.Resources;
     27 import android.graphics.drawable.Drawable;
     28 import android.net.ParseException;
     29 import android.net.Uri;
     30 import android.net.WebAddress;
     31 import android.os.Bundle;
     32 import android.os.Parcelable;
     33 import android.os.RemoteException;
     34 import android.os.ServiceManager;
     35 import android.provider.ContactsContract;
     36 import android.provider.ContactsContract.CommonDataKinds.Email;
     37 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     38 import android.provider.ContactsContract.CommonDataKinds.Im;
     39 import android.provider.ContactsContract.CommonDataKinds.Phone;
     40 import android.provider.ContactsContract.Contacts;
     41 import android.provider.ContactsContract.Data;
     42 import android.provider.ContactsContract.Directory;
     43 import android.provider.ContactsContract.DisplayNameSources;
     44 import android.provider.ContactsContract.StatusUpdates;
     45 import android.text.TextUtils;
     46 import android.util.Log;
     47 import android.view.ContextMenu;
     48 import android.view.ContextMenu.ContextMenuInfo;
     49 import android.view.DragEvent;
     50 import android.view.KeyEvent;
     51 import android.view.LayoutInflater;
     52 import android.view.MenuItem;
     53 import android.view.MotionEvent;
     54 import android.view.View;
     55 import android.view.View.OnClickListener;
     56 import android.view.View.OnDragListener;
     57 import android.view.View.OnTouchListener;
     58 import android.view.ViewGroup;
     59 import android.widget.AbsListView.OnScrollListener;
     60 import android.widget.AdapterView;
     61 import android.widget.AdapterView.AdapterContextMenuInfo;
     62 import android.widget.AdapterView.OnItemClickListener;
     63 import android.widget.BaseAdapter;
     64 import android.widget.Button;
     65 import android.widget.ImageView;
     66 import android.widget.ListAdapter;
     67 import android.widget.ListPopupWindow;
     68 import android.widget.ListView;
     69 import android.widget.TextView;
     70 
     71 import com.android.contacts.Collapser;
     72 import com.android.contacts.Collapser.Collapsible;
     73 import com.android.contacts.ContactPresenceIconUtil;
     74 import com.android.contacts.ContactSaveService;
     75 import com.android.contacts.ContactsUtils;
     76 import com.android.contacts.GroupMetaData;
     77 import com.android.contacts.R;
     78 import com.android.contacts.TypePrecedence;
     79 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
     80 import com.android.contacts.editor.SelectAccountDialogFragment;
     81 import com.android.contacts.model.AccountTypeManager;
     82 import com.android.contacts.model.Contact;
     83 import com.android.contacts.model.RawContact;
     84 import com.android.contacts.model.RawContactDelta;
     85 import com.android.contacts.model.RawContactDelta.ValuesDelta;
     86 import com.android.contacts.model.RawContactDeltaList;
     87 import com.android.contacts.model.RawContactModifier;
     88 import com.android.contacts.model.account.AccountType;
     89 import com.android.contacts.model.account.AccountType.EditType;
     90 import com.android.contacts.model.account.AccountWithDataSet;
     91 import com.android.contacts.model.dataitem.DataItem;
     92 import com.android.contacts.model.dataitem.DataKind;
     93 import com.android.contacts.model.dataitem.EmailDataItem;
     94 import com.android.contacts.model.dataitem.EventDataItem;
     95 import com.android.contacts.model.dataitem.GroupMembershipDataItem;
     96 import com.android.contacts.model.dataitem.ImDataItem;
     97 import com.android.contacts.model.dataitem.NicknameDataItem;
     98 import com.android.contacts.model.dataitem.NoteDataItem;
     99 import com.android.contacts.model.dataitem.OrganizationDataItem;
    100 import com.android.contacts.model.dataitem.PhoneDataItem;
    101 import com.android.contacts.model.dataitem.RelationDataItem;
    102 import com.android.contacts.model.dataitem.SipAddressDataItem;
    103 import com.android.contacts.model.dataitem.StructuredNameDataItem;
    104 import com.android.contacts.model.dataitem.StructuredPostalDataItem;
    105 import com.android.contacts.model.dataitem.WebsiteDataItem;
    106 import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
    107 import com.android.contacts.util.ClipboardUtils;
    108 import com.android.contacts.util.Constants;
    109 import com.android.contacts.util.DataStatus;
    110 import com.android.contacts.util.DateUtils;
    111 import com.android.contacts.util.PhoneCapabilityTester;
    112 import com.android.contacts.util.StructuredPostalUtils;
    113 import com.android.internal.telephony.ITelephony;
    114 import com.google.common.annotations.VisibleForTesting;
    115 import com.google.common.collect.Iterables;
    116 
    117 import java.util.ArrayList;
    118 import java.util.Collections;
    119 import java.util.HashMap;
    120 import java.util.List;
    121 import java.util.Map;
    122 
    123 public 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