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 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