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