1 /*
2  * Copyright (C) 2010 Google Inc.
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.loaderapp.fragments;
18 
19 import com.android.loaderapp.ContactHeaderWidget;
20 import com.android.loaderapp.R;
21 import com.android.loaderapp.model.Collapser;
22 import com.android.loaderapp.model.ContactLoader;
23 import com.android.loaderapp.model.ContactsSource;
24 import com.android.loaderapp.model.Sources;
25 import com.android.loaderapp.model.TypePrecedence;
26 import com.android.loaderapp.model.Collapser.Collapsible;
27 import com.android.loaderapp.model.ContactLoader.ContactData;
28 import com.android.loaderapp.model.ContactsSource.DataKind;
29 import com.android.loaderapp.util.Constants;
30 import com.android.loaderapp.util.ContactPresenceIconUtil;
31 import com.android.loaderapp.util.ContactsUtils;
32 import com.android.loaderapp.util.DataStatus;
33 import com.google.android.collect.Lists;
34 import com.google.android.collect.Maps;
35 
36 import android.app.LoaderManagingFragment;
37 import android.content.ActivityNotFoundException;
38 import android.content.ContentUris;
39 import android.content.ContentValues;
40 import android.content.Context;
41 import android.content.Entity;
42 import android.content.Intent;
43 import android.content.Loader;
44 import android.content.Entity.NamedContentValues;
45 import android.content.res.Resources;
46 import android.graphics.drawable.Drawable;
47 import android.net.ParseException;
48 import android.net.Uri;
49 import android.net.WebAddress;
50 import android.os.Bundle;
51 import android.provider.ContactsContract.CommonDataKinds;
52 import android.provider.ContactsContract.Contacts;
53 import android.provider.ContactsContract.Data;
54 import android.provider.ContactsContract.DisplayNameSources;
55 import android.provider.ContactsContract.RawContacts;
56 import android.provider.ContactsContract.StatusUpdates;
57 import android.provider.ContactsContract.CommonDataKinds.Email;
58 import android.provider.ContactsContract.CommonDataKinds.Im;
59 import android.provider.ContactsContract.CommonDataKinds.Nickname;
60 import android.provider.ContactsContract.CommonDataKinds.Note;
61 import android.provider.ContactsContract.CommonDataKinds.Organization;
62 import android.provider.ContactsContract.CommonDataKinds.Phone;
63 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
64 import android.provider.ContactsContract.CommonDataKinds.Website;
65 import android.telephony.PhoneNumberUtils;
66 import android.text.TextUtils;
67 import android.util.Log;
68 import android.view.LayoutInflater;
69 import android.view.View;
70 import android.view.ViewGroup;
71 import android.view.View.OnClickListener;
72 import android.widget.AdapterView;
73 import android.widget.ImageView;
74 import android.widget.ListView;
75 import android.widget.TextView;
76 import android.widget.AdapterView.OnItemClickListener;
77 
78 import java.util.ArrayList;
79 import java.util.HashMap;
80 
81 public class ContactFragment extends LoaderManagingFragment<ContactData>
82         implements OnClickListener, OnItemClickListener {
83     private static final String TAG = "ContactCoupler";
84 
85     static final String ARG_URI = "uri";
86     static final int LOADER_DETAILS = 1;
87 
88     Uri mUri;
89 
90     private static final boolean SHOW_SEPARATORS = false;
91 
92     protected Uri mLookupUri;
93     private ViewAdapter mAdapter;
94     private int mNumPhoneNumbers = 0;
95     private Controller mController;
96 
97     /**
98      * A list of distinct contact IDs included in the current contact.
99      */
100     private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
101 
102     /* package */ ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
103     /* package */ ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
104     /* package */ ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>();
105     /* package */ ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
106     /* package */ ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
107     /* package */ ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>();
108     /* package */ ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>();
109     /* package */ ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
110     /* package */ ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
111     /* package */ ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
112 
113     protected ContactHeaderWidget mContactHeaderWidget;
114 
115     protected LayoutInflater mInflater;
116 
117     protected int mReadOnlySourcesCnt;
118     protected int mWritableSourcesCnt;
119     protected boolean mAllRestricted;
120 
121     protected Uri mPrimaryPhoneUri = null;
122 
123     protected ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
124 
125     private long mNameRawContactId = -1;
126     private int mDisplayNameSource = DisplayNameSources.UNDEFINED;
127 
128     private ArrayList<Entity> mEntities = Lists.newArrayList();
129     private HashMap<Long, DataStatus> mStatuses = Maps.newHashMap();
130 
131     /**
132      * The view shown if the detail list is empty.
133      * We set this to the list view when first bind the adapter, so that it won't be shown while
134      * we're loading data.
135      */
136     private View mEmptyView;
137 
138     private ListView mListView;
139     private boolean mShowSmsLinksForAllPhones;
140 
ContactFragment()141     public ContactFragment() {
142     }
143 
ContactFragment(Uri uri, ContactFragment.Controller controller)144     public ContactFragment(Uri uri, ContactFragment.Controller controller) {
145         mUri = uri;
146         mController = controller;
147     }
148 
149     @Override
onCreate(Bundle savedState)150     public void onCreate(Bundle savedState) {
151         super.onCreate(savedState);
152     }
153 
154     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)155     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
156         View view = inflater.inflate(R.layout.contact_details, container, false);
157 
158         mInflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
159 
160         mContactHeaderWidget = (ContactHeaderWidget) view.findViewById(R.id.contact_header_widget);
161         mContactHeaderWidget.setExcludeMimes(new String[] { Contacts.CONTENT_ITEM_TYPE });
162 
163         mListView = (ListView) view.findViewById(android.R.id.list);
164         mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
165         mListView.setOnItemClickListener(this);
166         // Don't set it to mListView yet.  We do so later when we bind the adapter.
167         mEmptyView = view.findViewById(android.R.id.empty);
168 
169         // Build the list of sections. The order they're added to mSections dictates the
170         // order they are displayed in the list.
171         mSections.add(mPhoneEntries);
172         mSections.add(mSmsEntries);
173         mSections.add(mEmailEntries);
174         mSections.add(mImEntries);
175         mSections.add(mPostalEntries);
176         mSections.add(mNicknameEntries);
177         mSections.add(mOrganizationEntries);
178         mSections.add(mGroupEntries);
179         mSections.add(mOtherEntries);
180 
181         //TODO Read this value from a preference
182         mShowSmsLinksForAllPhones = true;
183 
184         return view;
185     }
186 
187     @Override
onInitializeLoaders()188     public void onInitializeLoaders() {
189         if (mUri != null) {
190             loadContact(mUri);
191         }
192     }
193 
194     @Override
onCreateLoader(int id, Bundle args)195     protected Loader onCreateLoader(int id, Bundle args) {
196         switch (id) {
197             case LOADER_DETAILS: {
198                 Uri uri = args.getParcelable(ARG_URI);
199                 return new ContactLoader(getActivity(), uri);
200             }
201         }
202         return null;
203     }
204 
205     @Override
onLoadFinished(Loader<ContactData> loader, ContactData data)206     public void onLoadFinished(Loader<ContactData> loader, ContactData data) {
207         switch (loader.getId()) {
208             case LOADER_DETAILS: {
209                 setData(data);
210                 break;
211             }
212         }
213     }
214 
loadContact(Uri uri)215     public void loadContact(Uri uri) {
216         mUri = uri;
217         Bundle args = new Bundle();
218         args.putParcelable(ARG_URI, uri);
219         startLoading(LOADER_DETAILS, args);
220     }
221 
setData(ContactData data)222     public void setData(ContactData data) {
223         mEntities = data.entities;
224         mStatuses = data.statuses;
225 
226         mNameRawContactId = data.nameRawContactId;
227         mDisplayNameSource = data.displayNameSource;
228 
229         mContactHeaderWidget.bindFromContactLookupUri(data.uri);
230         bindData();
231     }
232 
233     public interface Controller {
onPrimaryAction(ViewEntry entry)234         public void onPrimaryAction(ViewEntry entry);
onSecondaryAction(ViewEntry entry)235         public void onSecondaryAction(ViewEntry entry);
236     }
237 
238     public static final class DefaultController implements Controller {
239         private Context mContext;
240 
DefaultController(Context context)241         public DefaultController(Context context) {
242             mContext = context;
243         }
244 
onPrimaryAction(ViewEntry entry)245         public void onPrimaryAction(ViewEntry entry) {
246             Intent intent = entry.intent;
247             if (intent != null) {
248                 try {
249                     mContext.startActivity(intent);
250                 } catch (ActivityNotFoundException e) {
251                     Log.e(TAG, "No activity found for intent: " + intent);
252                 }
253             }
254         }
255 
onSecondaryAction(ViewEntry entry)256         public void onSecondaryAction(ViewEntry entry) {
257             Intent intent = entry.secondaryIntent;
258             if (intent != null) {
259                 try {
260                     mContext.startActivity(intent);
261                 } catch (ActivityNotFoundException e) {
262                     Log.e(TAG, "No activity found for intent: " + intent);
263                 }
264             }
265         }
266     }
267 
setController(Controller controller)268     public void setController(Controller controller) {
269         mController = controller;
270     }
271 
onItemClick(AdapterView parent, View v, int position, long id)272     public void onItemClick(AdapterView parent, View v, int position, long id) {
273         if (mController != null) {
274             ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
275             if (entry != null) {
276                 mController.onPrimaryAction(entry);
277             }
278         }
279     }
280 
onClick(View v)281     public void onClick(View v) {
282         if (mController != null) {
283             mController.onSecondaryAction((ViewEntry) v.getTag());
284         }
285     }
286 
bindData()287     private void bindData() {
288 
289         // Build up the contact entries
290         buildEntries();
291 
292         // Collapse similar data items in select sections.
293         Collapser.collapseList(mPhoneEntries);
294         Collapser.collapseList(mSmsEntries);
295         Collapser.collapseList(mEmailEntries);
296         Collapser.collapseList(mPostalEntries);
297         Collapser.collapseList(mImEntries);
298 
299         if (mAdapter == null) {
300             mAdapter = new ViewAdapter(getActivity(), mSections);
301             mListView.setAdapter(mAdapter);
302         } else {
303             mAdapter.setSections(mSections, SHOW_SEPARATORS);
304         }
305         mListView.setEmptyView(mEmptyView);
306     }
307 
308     /**
309      * Build up the entries to display on the screen.
310      *
311      * @param personCursor the URI for the contact being displayed
312      */
buildEntries()313     private final void buildEntries() {
314         // Clear out the old entries
315         final int numSections = mSections.size();
316         for (int i = 0; i < numSections; i++) {
317             mSections.get(i).clear();
318         }
319 
320         mRawContactIds.clear();
321 
322         mReadOnlySourcesCnt = 0;
323         mWritableSourcesCnt = 0;
324         mAllRestricted = true;
325         mPrimaryPhoneUri = null;
326 
327         mWritableRawContactIds.clear();
328 
329         if (mEntities == null || mStatuses == null) {
330             return;
331         }
332 
333         final Context context = getActivity();
334         final Sources sources = Sources.getInstance(context);
335 
336         // Build up method entries
337         for (Entity entity: mEntities) {
338             final ContentValues entValues = entity.getEntityValues();
339             final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
340             final long rawContactId = entValues.getAsLong(RawContacts._ID);
341 
342             // Mark when this contact has any unrestricted components
343             final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0;
344             if (!isRestricted) mAllRestricted = false;
345 
346             if (!mRawContactIds.contains(rawContactId)) {
347                 mRawContactIds.add(rawContactId);
348             }
349             ContactsSource contactsSource = sources.getInflatedSource(accountType,
350                     ContactsSource.LEVEL_SUMMARY);
351             if (contactsSource != null && contactsSource.readOnly) {
352                 mReadOnlySourcesCnt += 1;
353             } else {
354                 mWritableSourcesCnt += 1;
355                 mWritableRawContactIds.add(rawContactId);
356             }
357 
358 
359             for (NamedContentValues subValue : entity.getSubValues()) {
360                 final ContentValues entryValues = subValue.values;
361                 entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
362 
363                 final long dataId = entryValues.getAsLong(Data._ID);
364                 final String mimeType = entryValues.getAsString(Data.MIMETYPE);
365                 if (mimeType == null) continue;
366 
367                 final DataKind kind = sources.getKindOrFallback(accountType, mimeType,
368                         context, ContactsSource.LEVEL_MIMETYPES);
369                 if (kind == null) continue;
370 
371                 final ViewEntry entry = ViewEntry.fromValues(context, mimeType, kind,
372                         rawContactId, dataId, entryValues);
373 
374                 final boolean hasData = !TextUtils.isEmpty(entry.data);
375                 final boolean isSuperPrimary = entryValues.getAsInteger(
376                         Data.IS_SUPER_PRIMARY) != 0;
377 
378                 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
379                     // Build phone entries
380                     mNumPhoneNumbers++;
381 
382                     entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
383                             Uri.fromParts(Constants.SCHEME_TEL, entry.data, null));
384                     entry.secondaryIntent = new Intent(Intent.ACTION_SENDTO,
385                             Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null));
386 
387                     // Remember super-primary phone
388                     if (isSuperPrimary) mPrimaryPhoneUri = entry.uri;
389 
390                     entry.isPrimary = isSuperPrimary;
391                     mPhoneEntries.add(entry);
392 
393                     if (entry.type == CommonDataKinds.Phone.TYPE_MOBILE
394                             || mShowSmsLinksForAllPhones) {
395                         // Add an SMS entry
396                         if (kind.iconAltRes > 0) {
397                             entry.secondaryActionIcon = kind.iconAltRes;
398                         }
399                     }
400                 } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
401                     // Build email entries
402                     entry.intent = new Intent(Intent.ACTION_SENDTO,
403                             Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
404                     entry.isPrimary = isSuperPrimary;
405                     mEmailEntries.add(entry);
406 
407                     // When Email rows have status, create additional Im row
408                     final DataStatus status = mStatuses.get(entry.id);
409                     if (status != null) {
410                         final String imMime = Im.CONTENT_ITEM_TYPE;
411                         final DataKind imKind = sources.getKindOrFallback(accountType,
412                                 imMime, context, ContactsSource.LEVEL_MIMETYPES);
413                         final ViewEntry imEntry = ViewEntry.fromValues(context,
414                                 imMime, imKind, rawContactId, dataId, entryValues);
415                         imEntry.intent = ContactsUtils.buildImIntent(entryValues);
416                         imEntry.applyStatus(status, false);
417                         mImEntries.add(imEntry);
418                     }
419                 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
420                     // Build postal entries
421                     entry.maxLines = 4;
422                     entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
423                     mPostalEntries.add(entry);
424                 } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
425                     // Build IM entries
426                     entry.intent = ContactsUtils.buildImIntent(entryValues);
427                     if (TextUtils.isEmpty(entry.label)) {
428                         entry.label = context.getString(R.string.chat).toLowerCase();
429                     }
430 
431                     // Apply presence and status details when available
432                     final DataStatus status = mStatuses.get(entry.id);
433                     if (status != null) {
434                         entry.applyStatus(status, false);
435                     }
436                     mImEntries.add(entry);
437                 } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType) &&
438                         (hasData || !TextUtils.isEmpty(entry.label))) {
439                     // Build organization entries
440                     final boolean isNameRawContact = (mNameRawContactId == rawContactId);
441 
442                     final boolean duplicatesTitle =
443                         isNameRawContact
444                         && mDisplayNameSource == DisplayNameSources.ORGANIZATION
445                         && (!hasData || TextUtils.isEmpty(entry.label));
446 
447                     if (!duplicatesTitle) {
448                         entry.uri = null;
449 
450                         if (TextUtils.isEmpty(entry.label)) {
451                             entry.label = entry.data;
452                             entry.data = "";
453                         }
454 
455                         mOrganizationEntries.add(entry);
456                     }
457                 } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
458                     // Build nickname entries
459                     final boolean isNameRawContact = (mNameRawContactId == rawContactId);
460 
461                     final boolean duplicatesTitle =
462                         isNameRawContact
463                         && mDisplayNameSource == DisplayNameSources.NICKNAME;
464 
465                     if (!duplicatesTitle) {
466                         entry.uri = null;
467                         mNicknameEntries.add(entry);
468                     }
469                 } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
470                     // Build note entries
471                     entry.uri = null;
472                     entry.maxLines = 100;
473                     mOtherEntries.add(entry);
474                 } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
475                     // Build note entries
476                     entry.uri = null;
477                     entry.maxLines = 10;
478                     try {
479                         WebAddress webAddress = new WebAddress(entry.data);
480                         entry.intent = new Intent(Intent.ACTION_VIEW,
481                                 Uri.parse(webAddress.toString()));
482                     } catch (ParseException e) {
483                         Log.e(TAG, "Couldn't parse website: " + entry.data);
484                     }
485                     mOtherEntries.add(entry);
486                 } else {
487                     // Handle showing custom rows
488                     entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
489 
490                     // Use social summary when requested by external source
491                     final DataStatus status = mStatuses.get(entry.id);
492                     final boolean hasSocial = kind.actionBodySocial && status != null;
493                     if (hasSocial) {
494                         entry.applyStatus(status, true);
495                     }
496 
497                     if (hasSocial || hasData) {
498                         mOtherEntries.add(entry);
499                     }
500                 }
501             }
502         }
503     }
504 
buildActionString(DataKind kind, ContentValues values, boolean lowerCase, Context context)505     static String buildActionString(DataKind kind, ContentValues values, boolean lowerCase,
506             Context context) {
507         if (kind.actionHeader == null) {
508             return null;
509         }
510         CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
511         if (actionHeader == null) {
512             return null;
513         }
514         return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
515     }
516 
buildDataString(DataKind kind, ContentValues values, Context context)517     static String buildDataString(DataKind kind, ContentValues values, Context context) {
518         if (kind.actionBody == null) {
519             return null;
520         }
521         CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
522         return actionBody == null ? null : actionBody.toString();
523     }
524 
525     /**
526      * A basic structure with the data for a contact entry in the list.
527      */
528    public static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
529         public Context context = null;
530         public String resPackageName = null;
531         public int actionIcon = -1;
532         public boolean isPrimary = false;
533         public int secondaryActionIcon = -1;
534         public Intent intent;
535         public Intent secondaryIntent = null;
536         public int maxLabelLines = 1;
537         public ArrayList<Long> ids = new ArrayList<Long>();
538         public int collapseCount = 0;
539 
540         public int presence = -1;
541 
542         public CharSequence footerLine = null;
543 
ViewEntry()544         private ViewEntry() {
545         }
546 
547         /**
548          * Build new {@link ViewEntry} and populate from the given values.
549          */
fromValues(Context context, String mimeType, DataKind kind, long rawContactId, long dataId, ContentValues values)550         public static ViewEntry fromValues(Context context, String mimeType, DataKind kind,
551                 long rawContactId, long dataId, ContentValues values) {
552             final ViewEntry entry = new ViewEntry();
553             entry.context = context;
554             entry.contactId = rawContactId;
555             entry.id = dataId;
556             entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
557             entry.mimetype = mimeType;
558             entry.label = buildActionString(kind, values, false, context);
559             entry.data = buildDataString(kind, values, context);
560 
561             if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
562                 entry.type = values.getAsInteger(kind.typeColumn);
563             }
564             if (kind.iconRes > 0) {
565                 entry.resPackageName = kind.resPackageName;
566                 entry.actionIcon = kind.iconRes;
567             }
568 
569             return entry;
570         }
571 
572         /**
573          * Apply given {@link DataStatus} values over this {@link ViewEntry}
574          *
575          * @param fillData When true, the given status replaces {@link #data}
576          *            and {@link #footerLine}. Otherwise only {@link #presence}
577          *            is updated.
578          */
applyStatus(DataStatus status, boolean fillData)579         public ViewEntry applyStatus(DataStatus status, boolean fillData) {
580             presence = status.getPresence();
581             if (fillData && status.isValid()) {
582                 this.data = status.getStatus().toString();
583                 this.footerLine = status.getTimestampLabel(context);
584             }
585 
586             return this;
587         }
588 
collapseWith(ViewEntry entry)589         public boolean collapseWith(ViewEntry entry) {
590             // assert equal collapse keys
591             if (!shouldCollapseWith(entry)) {
592                 return false;
593             }
594 
595             // Choose the label associated with the highest type precedence.
596             if (TypePrecedence.getTypePrecedence(mimetype, type)
597                     > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) {
598                 type = entry.type;
599                 label = entry.label;
600             }
601 
602             // Choose the max of the maxLines and maxLabelLines values.
603             maxLines = Math.max(maxLines, entry.maxLines);
604             maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines);
605 
606             // Choose the presence with the highest precedence.
607             if (StatusUpdates.getPresencePrecedence(presence)
608                     < StatusUpdates.getPresencePrecedence(entry.presence)) {
609                 presence = entry.presence;
610             }
611 
612             // If any of the collapsed entries are primary make the whole thing primary.
613             isPrimary = entry.isPrimary ? true : isPrimary;
614 
615             // uri, and contactdId, shouldn't make a difference. Just keep the original.
616 
617             // Keep track of all the ids that have been collapsed with this one.
618             ids.add(entry.id);
619             collapseCount++;
620             return true;
621         }
622 
shouldCollapseWith(ViewEntry entry)623         public boolean shouldCollapseWith(ViewEntry entry) {
624             if (entry == null) {
625                 return false;
626             }
627 
628             if (!ContactsUtils.areDataEqual(context, mimetype, data, entry.mimetype, entry.data)) {
629                 return false;
630             }
631 
632             if (!TextUtils.equals(mimetype, entry.mimetype)
633                     || !ContactsUtils.areIntentActionEqual(intent, entry.intent)
634                     || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)
635                     || actionIcon != entry.actionIcon) {
636                 return false;
637             }
638 
639             return true;
640         }
641     }
642 
643     /** Cache of the children views of a row */
644     static class ViewCache {
645         public TextView label;
646         public TextView data;
647         public TextView footer;
648         public ImageView actionIcon;
649         public ImageView presenceIcon;
650         public ImageView primaryIcon;
651         public ImageView secondaryActionButton;
652         public View secondaryActionDivider;
653 
654         // Need to keep track of this too
655         ViewEntry entry;
656     }
657 
658     private final class ViewAdapter extends ContactEntryAdapter<ViewEntry> {
ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections)659         ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
660             super(context, sections, SHOW_SEPARATORS);
661         }
662 
663         @Override
getView(int position, View convertView, ViewGroup parent)664         public View getView(int position, View convertView, ViewGroup parent) {
665             ViewEntry entry = getEntry(mSections, position, false);
666             View v;
667 
668             ViewCache views;
669 
670             // Check to see if we can reuse convertView
671             if (convertView != null) {
672                 v = convertView;
673                 views = (ViewCache) v.getTag();
674             } else {
675                 // Create a new view if needed
676                 v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
677 
678                 // Cache the children
679                 views = new ViewCache();
680                 views.label = (TextView) v.findViewById(android.R.id.text1);
681                 views.data = (TextView) v.findViewById(android.R.id.text2);
682                 views.footer = (TextView) v.findViewById(R.id.footer);
683                 views.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
684                 views.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
685                 views.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
686                 views.secondaryActionButton = (ImageView) v.findViewById(
687                         R.id.secondary_action_button);
688                 views.secondaryActionButton.setOnClickListener(ContactFragment.this);
689                 views.secondaryActionDivider = v.findViewById(R.id.divider);
690                 v.setTag(views);
691             }
692 
693             // Update the entry in the view cache
694             views.entry = entry;
695 
696             // Bind the data to the view
697             bindView(v, entry);
698             return v;
699         }
700 
701         @Override
newView(int position, ViewGroup parent)702         protected View newView(int position, ViewGroup parent) {
703             // getView() handles this
704             throw new UnsupportedOperationException();
705         }
706 
707         @Override
bindView(View view, ViewEntry entry)708         protected void bindView(View view, ViewEntry entry) {
709             final Resources resources = mContext.getResources();
710             ViewCache views = (ViewCache) view.getTag();
711 
712             // Set the label
713             TextView label = views.label;
714             setMaxLines(label, entry.maxLabelLines);
715             label.setText(entry.label);
716 
717             // Set the data
718             TextView data = views.data;
719             if (data != null) {
720                 if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
721                         || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
722                     data.setText(PhoneNumberUtils.formatNumber(entry.data));
723                 } else {
724                     data.setText(entry.data);
725                 }
726                 setMaxLines(data, entry.maxLines);
727             }
728 
729             // Set the footer
730             if (!TextUtils.isEmpty(entry.footerLine)) {
731                 views.footer.setText(entry.footerLine);
732                 views.footer.setVisibility(View.VISIBLE);
733             } else {
734                 views.footer.setVisibility(View.GONE);
735             }
736 
737             // Set the primary icon
738             views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
739 
740             // Set the action icon
741             ImageView action = views.actionIcon;
742             if (entry.actionIcon != -1) {
743                 Drawable actionIcon;
744                 if (entry.resPackageName != null) {
745                     // Load external resources through PackageManager
746                     actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
747                             entry.actionIcon, null);
748                 } else {
749                     actionIcon = resources.getDrawable(entry.actionIcon);
750                 }
751                 action.setImageDrawable(actionIcon);
752                 action.setVisibility(View.VISIBLE);
753             } else {
754                 // Things should still line up as if there was an icon, so make it invisible
755                 action.setVisibility(View.INVISIBLE);
756             }
757 
758             // Set the presence icon
759             Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
760                     mContext, entry.presence);
761             ImageView presenceIconView = views.presenceIcon;
762             if (presenceIcon != null) {
763                 presenceIconView.setImageDrawable(presenceIcon);
764                 presenceIconView.setVisibility(View.VISIBLE);
765             } else {
766                 presenceIconView.setVisibility(View.GONE);
767             }
768 
769             // Set the secondary action button
770             ImageView secondaryActionView = views.secondaryActionButton;
771             Drawable secondaryActionIcon = null;
772             if (entry.secondaryActionIcon != -1) {
773                 secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
774             }
775             if (entry.secondaryIntent != null && secondaryActionIcon != null) {
776                 secondaryActionView.setImageDrawable(secondaryActionIcon);
777                 secondaryActionView.setTag(entry);
778                 secondaryActionView.setVisibility(View.VISIBLE);
779                 views.secondaryActionDivider.setVisibility(View.VISIBLE);
780             } else {
781                 secondaryActionView.setVisibility(View.GONE);
782                 views.secondaryActionDivider.setVisibility(View.GONE);
783             }
784         }
785 
setMaxLines(TextView textView, int maxLines)786         private void setMaxLines(TextView textView, int maxLines) {
787             if (maxLines == 1) {
788                 textView.setSingleLine(true);
789                 textView.setEllipsize(TextUtils.TruncateAt.END);
790             } else {
791                 textView.setSingleLine(false);
792                 textView.setMaxLines(maxLines);
793                 textView.setEllipsize(null);
794             }
795         }
796     }
797 }
798