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