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