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