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