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