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