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