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