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.loaderapp.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.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
188    public void onInitializeLoaders() {
189        if (mUri != null) {
190            loadContact(mUri);
191        }
192    }
193
194    @Override
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
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
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
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 {
234        public void onPrimaryAction(ViewEntry entry);
235        public void onSecondaryAction(ViewEntry entry);
236    }
237
238    public static final class DefaultController implements Controller {
239        private Context mContext;
240
241        public DefaultController(Context context) {
242            mContext = context;
243        }
244
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
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
268    public void setController(Controller controller) {
269        mController = controller;
270    }
271
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
281    public void onClick(View v) {
282        if (mController != null) {
283            mController.onSecondaryAction((ViewEntry) v.getTag());
284        }
285    }
286
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     */
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
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
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
544        private ViewEntry() {
545        }
546
547        /**
548         * Build new {@link ViewEntry} and populate from the given values.
549         */
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         */
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
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
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> {
659        ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
660            super(context, sections, SHOW_SEPARATORS);
661        }
662
663        @Override
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
702        protected View newView(int position, ViewGroup parent) {
703            // getView() handles this
704            throw new UnsupportedOperationException();
705        }
706
707        @Override
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
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