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