1 /* 2 * Copyright (C) 2007 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; 18 import com.android.contacts.Collapser.Collapsible; 19 import com.android.contacts.model.ContactsSource; 20 import com.android.contacts.model.Sources; 21 import com.android.contacts.model.ContactsSource.DataKind; 22 import com.android.contacts.ui.EditContactActivity; 23 import com.android.contacts.util.Constants; 24 import com.android.contacts.util.DataStatus; 25 import com.android.contacts.util.NotifyingAsyncQueryHandler; 26 import com.android.internal.telephony.ITelephony; 27 import com.android.internal.widget.ContactHeaderWidget; 28 import com.google.android.collect.Lists; 29 import com.google.android.collect.Maps; 30 31 import android.app.Activity; 32 import android.app.AlertDialog; 33 import android.app.Dialog; 34 import android.content.ActivityNotFoundException; 35 import android.content.ContentResolver; 36 import android.content.ContentUris; 37 import android.content.ContentValues; 38 import android.content.Context; 39 import android.content.DialogInterface; 40 import android.content.Entity; 41 import android.content.EntityIterator; 42 import android.content.Intent; 43 import android.content.Entity.NamedContentValues; 44 import android.content.res.Resources; 45 import android.database.ContentObserver; 46 import android.database.Cursor; 47 import android.graphics.drawable.Drawable; 48 import android.net.ParseException; 49 import android.net.Uri; 50 import android.net.WebAddress; 51 import android.os.AsyncTask; 52 import android.os.Bundle; 53 import android.os.Handler; 54 import android.os.RemoteException; 55 import android.os.ServiceManager; 56 import android.provider.ContactsContract; 57 import android.provider.ContactsContract.AggregationExceptions; 58 import android.provider.ContactsContract.CommonDataKinds; 59 import android.provider.ContactsContract.Contacts; 60 import android.provider.ContactsContract.Data; 61 import android.provider.ContactsContract.DisplayNameSources; 62 import android.provider.ContactsContract.RawContacts; 63 import android.provider.ContactsContract.RawContactsEntity; 64 import android.provider.ContactsContract.StatusUpdates; 65 import android.provider.ContactsContract.CommonDataKinds.Email; 66 import android.provider.ContactsContract.CommonDataKinds.Im; 67 import android.provider.ContactsContract.CommonDataKinds.Nickname; 68 import android.provider.ContactsContract.CommonDataKinds.Note; 69 import android.provider.ContactsContract.CommonDataKinds.Organization; 70 import android.provider.ContactsContract.CommonDataKinds.Phone; 71 import android.provider.ContactsContract.CommonDataKinds.SipAddress; 72 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 73 import android.provider.ContactsContract.CommonDataKinds.Website; 74 import android.telephony.PhoneNumberUtils; 75 import android.text.TextUtils; 76 import android.util.Log; 77 import android.view.ContextMenu; 78 import android.view.KeyEvent; 79 import android.view.LayoutInflater; 80 import android.view.Menu; 81 import android.view.MenuInflater; 82 import android.view.MenuItem; 83 import android.view.View; 84 import android.view.ViewGroup; 85 import android.view.Window; 86 import android.view.ContextMenu.ContextMenuInfo; 87 import android.widget.AdapterView; 88 import android.widget.ImageView; 89 import android.widget.ListView; 90 import android.widget.TextView; 91 import android.widget.Toast; 92 93 import java.util.ArrayList; 94 import java.util.HashMap; 95 import java.util.List; 96 97 /** 98 * Displays the details of a specific contact. 99 */ 100 public class ViewContactActivity extends Activity 101 implements View.OnCreateContextMenuListener, DialogInterface.OnClickListener, 102 AdapterView.OnItemClickListener, NotifyingAsyncQueryHandler.AsyncQueryListener { 103 private static final String TAG = "ViewContact"; 104 105 private static final boolean SHOW_SEPARATORS = false; 106 107 private static final int DIALOG_CONFIRM_DELETE = 1; 108 private static final int DIALOG_CONFIRM_READONLY_DELETE = 2; 109 private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3; 110 private static final int DIALOG_CONFIRM_READONLY_HIDE = 4; 111 112 private static final int REQUEST_JOIN_CONTACT = 1; 113 private static final int REQUEST_EDIT_CONTACT = 2; 114 115 public static final int MENU_ITEM_MAKE_DEFAULT = 3; 116 public static final int MENU_ITEM_CALL = 4; 117 118 protected Uri mLookupUri; 119 private ContentResolver mResolver; 120 private ViewAdapter mAdapter; 121 private int mNumPhoneNumbers = 0; 122 123 /** 124 * A list of distinct contact IDs included in the current contact. 125 */ 126 private ArrayList<Long> mRawContactIds = new ArrayList<Long>(); 127 128 /* package */ ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>(); 129 /* package */ ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>(); 130 /* package */ ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>(); 131 /* package */ ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>(); 132 /* package */ ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>(); 133 /* package */ ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>(); 134 /* package */ ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>(); 135 /* package */ ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>(); 136 /* package */ ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>(); 137 /* package */ ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>(); 138 139 private Cursor mCursor; 140 141 protected ContactHeaderWidget mContactHeaderWidget; 142 private NotifyingAsyncQueryHandler mHandler; 143 144 protected LayoutInflater mInflater; 145 146 protected int mReadOnlySourcesCnt; 147 protected int mWritableSourcesCnt; 148 protected boolean mAllRestricted; 149 150 protected Uri mPrimaryPhoneUri = null; 151 152 protected ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>(); 153 154 private static final int TOKEN_ENTITIES = 0; 155 private static final int TOKEN_STATUSES = 1; 156 157 private boolean mHasEntities = false; 158 private boolean mHasStatuses = false; 159 160 private long mNameRawContactId = -1; 161 private int mDisplayNameSource = DisplayNameSources.UNDEFINED; 162 163 private ArrayList<Entity> mEntities = Lists.newArrayList(); 164 private HashMap<Long, DataStatus> mStatuses = Maps.newHashMap(); 165 166 /** 167 * The view shown if the detail list is empty. 168 * We set this to the list view when first bind the adapter, so that it won't be shown while 169 * we're loading data. 170 */ 171 private View mEmptyView; 172 173 private ContentObserver mObserver = new ContentObserver(new Handler()) { 174 @Override 175 public boolean deliverSelfNotifications() { 176 return true; 177 } 178 179 @Override 180 public void onChange(boolean selfChange) { 181 if (mCursor != null && !mCursor.isClosed()) { 182 startEntityQuery(); 183 } 184 } 185 }; 186 187 public void onClick(DialogInterface dialog, int which) { 188 closeCursor(); 189 getContentResolver().delete(mLookupUri, null, null); 190 finish(); 191 } 192 193 private ListView mListView; 194 private boolean mShowSmsLinksForAllPhones; 195 196 @Override 197 protected void onCreate(Bundle icicle) { 198 super.onCreate(icicle); 199 200 final Intent intent = getIntent(); 201 Uri data = intent.getData(); 202 String authority = data.getAuthority(); 203 if (ContactsContract.AUTHORITY.equals(authority)) { 204 mLookupUri = data; 205 } else if (android.provider.Contacts.AUTHORITY.equals(authority)) { 206 final long rawContactId = ContentUris.parseId(data); 207 mLookupUri = RawContacts.getContactLookupUri(getContentResolver(), 208 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 209 210 } 211 mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 212 213 requestWindowFeature(Window.FEATURE_NO_TITLE); 214 setContentView(R.layout.contact_card_layout); 215 216 mContactHeaderWidget = (ContactHeaderWidget) findViewById(R.id.contact_header_widget); 217 mContactHeaderWidget.showStar(true); 218 mContactHeaderWidget.setExcludeMimes(new String[] { 219 Contacts.CONTENT_ITEM_TYPE 220 }); 221 mContactHeaderWidget.setSelectedContactsAppTabIndex(StickyTabs.getTab(getIntent())); 222 223 mHandler = new NotifyingAsyncQueryHandler(this, this); 224 225 mListView = (ListView) findViewById(R.id.contact_data); 226 mListView.setOnCreateContextMenuListener(this); 227 mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); 228 mListView.setOnItemClickListener(this); 229 // Don't set it to mListView yet. We do so later when we bind the adapter. 230 mEmptyView = findViewById(android.R.id.empty); 231 232 mResolver = getContentResolver(); 233 234 // Build the list of sections. The order they're added to mSections dictates the 235 // order they are displayed in the list. 236 mSections.add(mPhoneEntries); 237 mSections.add(mSmsEntries); 238 mSections.add(mEmailEntries); 239 mSections.add(mImEntries); 240 mSections.add(mPostalEntries); 241 mSections.add(mNicknameEntries); 242 mSections.add(mOrganizationEntries); 243 mSections.add(mGroupEntries); 244 mSections.add(mOtherEntries); 245 246 //TODO Read this value from a preference 247 mShowSmsLinksForAllPhones = true; 248 } 249 250 @Override 251 protected void onResume() { 252 super.onResume(); 253 startEntityQuery(); 254 } 255 256 @Override 257 protected void onPause() { 258 super.onPause(); 259 closeCursor(); 260 } 261 262 @Override 263 protected void onDestroy() { 264 super.onDestroy(); 265 closeCursor(); 266 } 267 268 @Override 269 protected Dialog onCreateDialog(int id) { 270 switch (id) { 271 case DIALOG_CONFIRM_DELETE: 272 return new AlertDialog.Builder(this) 273 .setTitle(R.string.deleteConfirmation_title) 274 .setIcon(android.R.drawable.ic_dialog_alert) 275 .setMessage(R.string.deleteConfirmation) 276 .setNegativeButton(android.R.string.cancel, null) 277 .setPositiveButton(android.R.string.ok, this) 278 .setCancelable(false) 279 .create(); 280 case DIALOG_CONFIRM_READONLY_DELETE: 281 return new AlertDialog.Builder(this) 282 .setTitle(R.string.deleteConfirmation_title) 283 .setIcon(android.R.drawable.ic_dialog_alert) 284 .setMessage(R.string.readOnlyContactDeleteConfirmation) 285 .setNegativeButton(android.R.string.cancel, null) 286 .setPositiveButton(android.R.string.ok, this) 287 .setCancelable(false) 288 .create(); 289 case DIALOG_CONFIRM_MULTIPLE_DELETE: 290 return new AlertDialog.Builder(this) 291 .setTitle(R.string.deleteConfirmation_title) 292 .setIcon(android.R.drawable.ic_dialog_alert) 293 .setMessage(R.string.multipleContactDeleteConfirmation) 294 .setNegativeButton(android.R.string.cancel, null) 295 .setPositiveButton(android.R.string.ok, this) 296 .setCancelable(false) 297 .create(); 298 case DIALOG_CONFIRM_READONLY_HIDE: { 299 return new AlertDialog.Builder(this) 300 .setTitle(R.string.deleteConfirmation_title) 301 .setIcon(android.R.drawable.ic_dialog_alert) 302 .setMessage(R.string.readOnlyContactWarning) 303 .setPositiveButton(android.R.string.ok, this) 304 .create(); 305 } 306 307 } 308 return null; 309 } 310 311 /** {@inheritDoc} */ 312 public void onQueryComplete(int token, Object cookie, final Cursor cursor) { 313 if (token == TOKEN_STATUSES) { 314 try { 315 // Read available social rows and consider binding 316 readStatuses(cursor); 317 } finally { 318 if (cursor != null) { 319 cursor.close(); 320 } 321 } 322 considerBindData(); 323 return; 324 } 325 326 // One would think we could just iterate over the Cursor 327 // directly here, as the result set should be small, and we've 328 // already run the query in an AsyncTask, but a lot of ANRs 329 // were being reported in this code nonetheless. See bug 330 // 2539603 for details. The real bug which makes this result 331 // set huge and CPU-heavy may be elsewhere. 332 // TODO: if we keep this async, perhaps the entity iteration 333 // should also be original AsyncTask, rather than ping-ponging 334 // between threads like this. 335 final ArrayList<Entity> oldEntities = mEntities; 336 (new AsyncTask<Void, Void, ArrayList<Entity>>() { 337 @Override 338 protected ArrayList<Entity> doInBackground(Void... params) { 339 ArrayList<Entity> newEntities = new ArrayList<Entity>(cursor.getCount()); 340 EntityIterator iterator = RawContacts.newEntityIterator(cursor); 341 try { 342 while (iterator.hasNext()) { 343 Entity entity = iterator.next(); 344 newEntities.add(entity); 345 } 346 } finally { 347 iterator.close(); 348 } 349 return newEntities; 350 } 351 352 @Override 353 protected void onPostExecute(ArrayList<Entity> newEntities) { 354 if (newEntities == null) { 355 // There was an error loading. 356 return; 357 } 358 synchronized (ViewContactActivity.this) { 359 if (mEntities != oldEntities) { 360 // Multiple async tasks were in flight and we 361 // lost the race. 362 return; 363 } 364 mEntities = newEntities; 365 mHasEntities = true; 366 } 367 considerBindData(); 368 } 369 }).execute(); 370 } 371 372 private long getRefreshedContactId() { 373 Uri freshContactUri = Contacts.lookupContact(getContentResolver(), mLookupUri); 374 if (freshContactUri != null) { 375 return ContentUris.parseId(freshContactUri); 376 } 377 return -1; 378 } 379 380 /** 381 * Read from the given {@link Cursor} and build a set of {@link DataStatus} 382 * objects to match any valid statuses found. 383 */ 384 private synchronized void readStatuses(Cursor cursor) { 385 mStatuses.clear(); 386 387 // Walk found statuses, creating internal row for each 388 while (cursor.moveToNext()) { 389 final DataStatus status = new DataStatus(cursor); 390 final long dataId = cursor.getLong(StatusQuery._ID); 391 mStatuses.put(dataId, status); 392 } 393 394 mHasStatuses = true; 395 } 396 397 private static Cursor setupContactCursor(ContentResolver resolver, Uri lookupUri) { 398 if (lookupUri == null) { 399 return null; 400 } 401 final List<String> segments = lookupUri.getPathSegments(); 402 if (segments.size() != 4) { 403 return null; 404 } 405 406 // Contains an Id. 407 final long uriContactId = Long.parseLong(segments.get(3)); 408 final String uriLookupKey = Uri.encode(segments.get(2)); 409 final Uri dataUri = Uri.withAppendedPath( 410 ContentUris.withAppendedId(Contacts.CONTENT_URI, uriContactId), 411 Contacts.Data.CONTENT_DIRECTORY); 412 413 // This cursor has several purposes: 414 // - Fetch NAME_RAW_CONTACT_ID and DISPLAY_NAME_SOURCE 415 // - Fetch the lookup-key to ensure we are looking at the right record 416 // - Watcher for change events 417 Cursor cursor = resolver.query(dataUri, 418 new String[] { 419 Contacts.NAME_RAW_CONTACT_ID, 420 Contacts.DISPLAY_NAME_SOURCE, 421 Contacts.LOOKUP_KEY 422 }, null, null, null); 423 424 if (cursor.moveToFirst()) { 425 String lookupKey = 426 cursor.getString(cursor.getColumnIndex(Contacts.LOOKUP_KEY)); 427 if (!lookupKey.equals(uriLookupKey)) { 428 // ID and lookup key do not match 429 cursor.close(); 430 return null; 431 } 432 return cursor; 433 } else { 434 cursor.close(); 435 return null; 436 } 437 } 438 439 private synchronized void startEntityQuery() { 440 closeCursor(); 441 442 // Interprete mLookupUri 443 mCursor = setupContactCursor(mResolver, mLookupUri); 444 445 // If mCursor is null now we did not succeed in using the Uri's Id (or it didn't contain 446 // a Uri). Instead we now have to use the lookup key to find the record 447 if (mCursor == null) { 448 mLookupUri = Contacts.getLookupUri(getContentResolver(), mLookupUri); 449 mCursor = setupContactCursor(mResolver, mLookupUri); 450 } 451 452 // If mCursor is still null, we were unsuccessful in finding the record 453 if (mCursor == null) { 454 mNameRawContactId = -1; 455 mDisplayNameSource = DisplayNameSources.UNDEFINED; 456 // TODO either figure out a way to prevent a flash of black background or 457 // use some other UI than a toast 458 Toast.makeText(this, R.string.invalidContactMessage, Toast.LENGTH_SHORT).show(); 459 Log.e(TAG, "invalid contact uri: " + mLookupUri); 460 finish(); 461 return; 462 } 463 464 final long contactId = ContentUris.parseId(mLookupUri); 465 466 mNameRawContactId = 467 mCursor.getLong(mCursor.getColumnIndex(Contacts.NAME_RAW_CONTACT_ID)); 468 mDisplayNameSource = 469 mCursor.getInt(mCursor.getColumnIndex(Contacts.DISPLAY_NAME_SOURCE)); 470 471 mCursor.registerContentObserver(mObserver); 472 473 // Clear flags and start queries to data and status 474 mHasEntities = false; 475 mHasStatuses = false; 476 477 mHandler.startQuery(TOKEN_ENTITIES, null, RawContactsEntity.CONTENT_URI, null, 478 RawContacts.CONTACT_ID + "=?", new String[] { 479 String.valueOf(contactId) 480 }, null); 481 final Uri dataUri = Uri.withAppendedPath( 482 ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), 483 Contacts.Data.CONTENT_DIRECTORY); 484 mHandler.startQuery(TOKEN_STATUSES, null, dataUri, StatusQuery.PROJECTION, 485 StatusUpdates.PRESENCE + " IS NOT NULL OR " + StatusUpdates.STATUS 486 + " IS NOT NULL", null, null); 487 488 mContactHeaderWidget.bindFromContactLookupUri(mLookupUri); 489 } 490 491 private void closeCursor() { 492 if (mCursor != null) { 493 mCursor.unregisterContentObserver(mObserver); 494 mCursor.close(); 495 mCursor = null; 496 } 497 } 498 499 /** 500 * Consider binding views after any of several background queries has 501 * completed. We check internal flags and only bind when all data has 502 * arrived. 503 */ 504 private void considerBindData() { 505 if (mHasEntities && mHasStatuses) { 506 bindData(); 507 } 508 } 509 510 private void bindData() { 511 512 // Build up the contact entries 513 buildEntries(); 514 515 // Collapse similar data items in select sections. 516 Collapser.collapseList(mPhoneEntries); 517 Collapser.collapseList(mSmsEntries); 518 Collapser.collapseList(mEmailEntries); 519 Collapser.collapseList(mPostalEntries); 520 Collapser.collapseList(mImEntries); 521 522 if (mAdapter == null) { 523 mAdapter = new ViewAdapter(this, mSections); 524 mListView.setAdapter(mAdapter); 525 } else { 526 mAdapter.setSections(mSections, SHOW_SEPARATORS); 527 } 528 mListView.setEmptyView(mEmptyView); 529 } 530 531 @Override 532 public boolean onCreateOptionsMenu(Menu menu) { 533 super.onCreateOptionsMenu(menu); 534 535 final MenuInflater inflater = getMenuInflater(); 536 inflater.inflate(R.menu.view, menu); 537 return true; 538 } 539 540 @Override 541 public boolean onPrepareOptionsMenu(Menu menu) { 542 super.onPrepareOptionsMenu(menu); 543 544 // Only allow edit when we have at least one raw_contact id 545 final boolean hasRawContact = (mRawContactIds.size() > 0); 546 menu.findItem(R.id.menu_edit).setEnabled(hasRawContact); 547 548 // Only allow share when unrestricted contacts available 549 menu.findItem(R.id.menu_share).setEnabled(!mAllRestricted); 550 551 return true; 552 } 553 554 @Override 555 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 556 AdapterView.AdapterContextMenuInfo info; 557 try { 558 info = (AdapterView.AdapterContextMenuInfo) menuInfo; 559 } catch (ClassCastException e) { 560 Log.e(TAG, "bad menuInfo", e); 561 return; 562 } 563 564 // This can be null sometimes, don't crash... 565 if (info == null) { 566 Log.e(TAG, "bad menuInfo"); 567 return; 568 } 569 570 ViewEntry entry = ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS); 571 menu.setHeaderTitle(R.string.contactOptionsTitle); 572 if (entry.mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) { 573 menu.add(0, MENU_ITEM_CALL, 0, R.string.menu_call).setIntent(entry.intent); 574 menu.add(0, 0, 0, R.string.menu_sendSMS).setIntent(entry.secondaryIntent); 575 if (!entry.isPrimary) { 576 menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultNumber); 577 } 578 } else if (entry.mimetype.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) { 579 menu.add(0, 0, 0, R.string.menu_sendEmail).setIntent(entry.intent); 580 if (!entry.isPrimary) { 581 menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultEmail); 582 } 583 } else if (entry.mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)) { 584 menu.add(0, 0, 0, R.string.menu_viewAddress).setIntent(entry.intent); 585 } 586 } 587 588 @Override 589 public boolean onOptionsItemSelected(MenuItem item) { 590 switch (item.getItemId()) { 591 case R.id.menu_edit: { 592 Long rawContactIdToEdit = null; 593 if (mRawContactIds.size() > 0) { 594 rawContactIdToEdit = mRawContactIds.get(0); 595 } else { 596 // There is no rawContact to edit. 597 break; 598 } 599 Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, 600 rawContactIdToEdit); 601 startActivityForResult(new Intent(Intent.ACTION_EDIT, rawContactUri), 602 REQUEST_EDIT_CONTACT); 603 break; 604 } 605 case R.id.menu_delete: { 606 // Get confirmation 607 if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) { 608 showDialog(DIALOG_CONFIRM_READONLY_DELETE); 609 } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) { 610 showDialog(DIALOG_CONFIRM_READONLY_HIDE); 611 } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) { 612 showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE); 613 } else { 614 showDialog(DIALOG_CONFIRM_DELETE); 615 } 616 return true; 617 } 618 case R.id.menu_join: { 619 showJoinAggregateActivity(); 620 return true; 621 } 622 case R.id.menu_options: { 623 showOptionsActivity(); 624 return true; 625 } 626 case R.id.menu_share: { 627 if (mAllRestricted) return false; 628 629 // TODO: Keep around actual LOOKUP_KEY, or formalize method of extracting 630 final String lookupKey = Uri.encode(mLookupUri.getPathSegments().get(2)); 631 final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); 632 633 final Intent intent = new Intent(Intent.ACTION_SEND); 634 intent.setType(Contacts.CONTENT_VCARD_TYPE); 635 intent.putExtra(Intent.EXTRA_STREAM, shareUri); 636 637 // Launch chooser to share contact via 638 final CharSequence chooseTitle = getText(R.string.share_via); 639 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle); 640 641 try { 642 startActivity(chooseIntent); 643 } catch (ActivityNotFoundException ex) { 644 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 645 } 646 return true; 647 } 648 } 649 return super.onOptionsItemSelected(item); 650 } 651 652 @Override 653 public boolean onContextItemSelected(MenuItem item) { 654 switch (item.getItemId()) { 655 case MENU_ITEM_MAKE_DEFAULT: { 656 makeItemDefault(item); 657 return true; 658 } 659 case MENU_ITEM_CALL: { 660 StickyTabs.saveTab(this, getIntent()); 661 startActivity(item.getIntent()); 662 return true; 663 } 664 default: { 665 return super.onContextItemSelected(item); 666 } 667 } 668 } 669 670 private boolean makeItemDefault(MenuItem item) { 671 ViewEntry entry = getViewEntryForMenuItem(item); 672 if (entry == null) { 673 return false; 674 } 675 676 // Update the primary values in the data record. 677 ContentValues values = new ContentValues(1); 678 values.put(Data.IS_SUPER_PRIMARY, 1); 679 getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id), 680 values, null, null); 681 startEntityQuery(); 682 return true; 683 } 684 685 /** 686 * Shows a list of aggregates that can be joined into the currently viewed aggregate. 687 */ 688 public void showJoinAggregateActivity() { 689 long freshId = getRefreshedContactId(); 690 if (freshId > 0) { 691 String displayName = null; 692 if (mCursor.moveToFirst()) { 693 displayName = mCursor.getString(0); 694 } 695 Intent intent = new Intent(ContactsListActivity.JOIN_AGGREGATE); 696 intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_ID, freshId); 697 if (displayName != null) { 698 intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_NAME, displayName); 699 } 700 startActivityForResult(intent, REQUEST_JOIN_CONTACT); 701 } 702 } 703 704 @Override 705 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 706 if (requestCode == REQUEST_JOIN_CONTACT) { 707 if (resultCode == RESULT_OK && intent != null) { 708 final long contactId = ContentUris.parseId(intent.getData()); 709 joinAggregate(contactId); 710 } 711 } else if (requestCode == REQUEST_EDIT_CONTACT) { 712 if (resultCode == EditContactActivity.RESULT_CLOSE_VIEW_ACTIVITY) { 713 finish(); 714 } else if (resultCode == Activity.RESULT_OK) { 715 mLookupUri = intent.getData(); 716 if (mLookupUri == null) { 717 finish(); 718 } 719 } 720 } 721 } 722 723 private void joinAggregate(final long contactId) { 724 Cursor c = mResolver.query(RawContacts.CONTENT_URI, new String[] {RawContacts._ID}, 725 RawContacts.CONTACT_ID + "=" + contactId, null, null); 726 727 try { 728 while(c.moveToNext()) { 729 long rawContactId = c.getLong(0); 730 setAggregationException(rawContactId, AggregationExceptions.TYPE_KEEP_TOGETHER); 731 } 732 } finally { 733 c.close(); 734 } 735 736 Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show(); 737 startEntityQuery(); 738 } 739 740 /** 741 * Given a contact ID sets an aggregation exception to either join the contact with the 742 * current aggregate or split off. 743 */ 744 protected void setAggregationException(long rawContactId, int exceptionType) { 745 ContentValues values = new ContentValues(3); 746 for (long aRawContactId : mRawContactIds) { 747 if (aRawContactId != rawContactId) { 748 values.put(AggregationExceptions.RAW_CONTACT_ID1, aRawContactId); 749 values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId); 750 values.put(AggregationExceptions.TYPE, exceptionType); 751 mResolver.update(AggregationExceptions.CONTENT_URI, values, null, null); 752 } 753 } 754 } 755 756 private void showOptionsActivity() { 757 final Intent intent = new Intent(this, ContactOptionsActivity.class); 758 intent.setData(mLookupUri); 759 startActivity(intent); 760 } 761 762 private ViewEntry getViewEntryForMenuItem(MenuItem item) { 763 AdapterView.AdapterContextMenuInfo info; 764 try { 765 info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 766 } catch (ClassCastException e) { 767 Log.e(TAG, "bad menuInfo", e); 768 return null; 769 } 770 771 return ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS); 772 } 773 774 @Override 775 public boolean onKeyDown(int keyCode, KeyEvent event) { 776 switch (keyCode) { 777 case KeyEvent.KEYCODE_CALL: { 778 try { 779 ITelephony phone = ITelephony.Stub.asInterface( 780 ServiceManager.checkService("phone")); 781 if (phone != null && !phone.isIdle()) { 782 // Skip out and let the key be handled at a higher level 783 break; 784 } 785 } catch (RemoteException re) { 786 // Fall through and try to call the contact 787 } 788 789 int index = mListView.getSelectedItemPosition(); 790 if (index != -1) { 791 ViewEntry entry = ViewAdapter.getEntry(mSections, index, SHOW_SEPARATORS); 792 if (entry != null && entry.intent != null && 793 entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) { 794 startActivity(entry.intent); 795 StickyTabs.saveTab(this, getIntent()); 796 return true; 797 } 798 } else if (mPrimaryPhoneUri != null) { 799 // There isn't anything selected, call the default number 800 final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 801 mPrimaryPhoneUri); 802 startActivity(intent); 803 StickyTabs.saveTab(this, getIntent()); 804 return true; 805 } 806 return false; 807 } 808 809 case KeyEvent.KEYCODE_DEL: { 810 if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) { 811 showDialog(DIALOG_CONFIRM_READONLY_DELETE); 812 } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) { 813 showDialog(DIALOG_CONFIRM_READONLY_HIDE); 814 } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) { 815 showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE); 816 } else { 817 showDialog(DIALOG_CONFIRM_DELETE); 818 } 819 return true; 820 } 821 } 822 823 return super.onKeyDown(keyCode, event); 824 } 825 826 public void onItemClick(AdapterView parent, View v, int position, long id) { 827 ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS); 828 if (entry != null) { 829 Intent intent = entry.intent; 830 if (intent != null) { 831 if (Intent.ACTION_CALL_PRIVILEGED.equals(intent.getAction())) { 832 StickyTabs.saveTab(this, getIntent()); 833 } 834 try { 835 startActivity(intent); 836 } catch (ActivityNotFoundException e) { 837 Log.e(TAG, "No activity found for intent: " + intent); 838 signalError(); 839 } 840 } else { 841 signalError(); 842 } 843 } else { 844 signalError(); 845 } 846 } 847 848 /** 849 * Signal an error to the user via a beep, or some other method. 850 */ 851 private void signalError() { 852 //TODO: implement this when we have the sonification APIs 853 } 854 855 /** 856 * Build up the entries to display on the screen. 857 * 858 * @param personCursor the URI for the contact being displayed 859 */ 860 private final void buildEntries() { 861 // Clear out the old entries 862 final int numSections = mSections.size(); 863 for (int i = 0; i < numSections; i++) { 864 mSections.get(i).clear(); 865 } 866 867 mRawContactIds.clear(); 868 869 mReadOnlySourcesCnt = 0; 870 mWritableSourcesCnt = 0; 871 mAllRestricted = true; 872 mPrimaryPhoneUri = null; 873 874 mWritableRawContactIds.clear(); 875 876 final Context context = this; 877 final Sources sources = Sources.getInstance(context); 878 879 // Build up method entries 880 if (mLookupUri != null) { 881 for (Entity entity: mEntities) { 882 final ContentValues entValues = entity.getEntityValues(); 883 final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE); 884 final long rawContactId = entValues.getAsLong(RawContacts._ID); 885 886 // Mark when this contact has any unrestricted components 887 final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0; 888 if (!isRestricted) mAllRestricted = false; 889 890 if (!mRawContactIds.contains(rawContactId)) { 891 mRawContactIds.add(rawContactId); 892 } 893 ContactsSource contactsSource = sources.getInflatedSource(accountType, 894 ContactsSource.LEVEL_SUMMARY); 895 if (contactsSource != null && contactsSource.readOnly) { 896 mReadOnlySourcesCnt += 1; 897 } else { 898 mWritableSourcesCnt += 1; 899 mWritableRawContactIds.add(rawContactId); 900 } 901 902 903 for (NamedContentValues subValue : entity.getSubValues()) { 904 final ContentValues entryValues = subValue.values; 905 entryValues.put(Data.RAW_CONTACT_ID, rawContactId); 906 907 final long dataId = entryValues.getAsLong(Data._ID); 908 final String mimeType = entryValues.getAsString(Data.MIMETYPE); 909 if (mimeType == null) continue; 910 911 final DataKind kind = sources.getKindOrFallback(accountType, mimeType, this, 912 ContactsSource.LEVEL_MIMETYPES); 913 if (kind == null) continue; 914 915 final ViewEntry entry = ViewEntry.fromValues(context, mimeType, kind, 916 rawContactId, dataId, entryValues); 917 918 final boolean hasData = !TextUtils.isEmpty(entry.data); 919 final boolean isSuperPrimary = entryValues.getAsInteger( 920 Data.IS_SUPER_PRIMARY) != 0; 921 922 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 923 // Build phone entries 924 mNumPhoneNumbers++; 925 926 entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 927 Uri.fromParts(Constants.SCHEME_TEL, entry.data, null)); 928 entry.secondaryIntent = new Intent(Intent.ACTION_SENDTO, 929 Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null)); 930 931 // Remember super-primary phone 932 if (isSuperPrimary) mPrimaryPhoneUri = entry.uri; 933 934 entry.isPrimary = isSuperPrimary; 935 mPhoneEntries.add(entry); 936 937 if (entry.type == CommonDataKinds.Phone.TYPE_MOBILE 938 || mShowSmsLinksForAllPhones) { 939 // Add an SMS entry 940 if (kind.iconAltRes > 0) { 941 entry.secondaryActionIcon = kind.iconAltRes; 942 } 943 } 944 } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 945 // Build email entries 946 entry.intent = new Intent(Intent.ACTION_SENDTO, 947 Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null)); 948 entry.isPrimary = isSuperPrimary; 949 mEmailEntries.add(entry); 950 951 // When Email rows have status, create additional Im row 952 final DataStatus status = mStatuses.get(entry.id); 953 if (status != null) { 954 final String imMime = Im.CONTENT_ITEM_TYPE; 955 final DataKind imKind = sources.getKindOrFallback(accountType, 956 imMime, this, ContactsSource.LEVEL_MIMETYPES); 957 final ViewEntry imEntry = ViewEntry.fromValues(context, 958 imMime, imKind, rawContactId, dataId, entryValues); 959 imEntry.intent = ContactsUtils.buildImIntent(entryValues); 960 imEntry.applyStatus(status, false); 961 mImEntries.add(imEntry); 962 } 963 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 964 // Build postal entries 965 entry.maxLines = 4; 966 entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri); 967 mPostalEntries.add(entry); 968 } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 969 // Build IM entries 970 entry.intent = ContactsUtils.buildImIntent(entryValues); 971 if (TextUtils.isEmpty(entry.label)) { 972 entry.label = getString(R.string.chat).toLowerCase(); 973 } 974 975 // Apply presence and status details when available 976 final DataStatus status = mStatuses.get(entry.id); 977 if (status != null) { 978 entry.applyStatus(status, false); 979 } 980 mImEntries.add(entry); 981 } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType) && 982 (hasData || !TextUtils.isEmpty(entry.label))) { 983 // Build organization entries 984 final boolean isNameRawContact = (mNameRawContactId == rawContactId); 985 986 final boolean duplicatesTitle = 987 isNameRawContact 988 && mDisplayNameSource == DisplayNameSources.ORGANIZATION 989 && (!hasData || TextUtils.isEmpty(entry.label)); 990 991 if (!duplicatesTitle) { 992 entry.uri = null; 993 994 if (TextUtils.isEmpty(entry.label)) { 995 entry.label = entry.data; 996 entry.data = ""; 997 } 998 999 mOrganizationEntries.add(entry); 1000 } 1001 } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 1002 // Build nickname entries 1003 final boolean isNameRawContact = (mNameRawContactId == rawContactId); 1004 1005 final boolean duplicatesTitle = 1006 isNameRawContact 1007 && mDisplayNameSource == DisplayNameSources.NICKNAME; 1008 1009 if (!duplicatesTitle) { 1010 entry.uri = null; 1011 mNicknameEntries.add(entry); 1012 } 1013 } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 1014 // Build note entries 1015 entry.uri = null; 1016 entry.maxLines = 100; 1017 mOtherEntries.add(entry); 1018 } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 1019 // Build Website entries 1020 entry.uri = null; 1021 entry.maxLines = 10; 1022 try { 1023 WebAddress webAddress = new WebAddress(entry.data); 1024 entry.intent = new Intent(Intent.ACTION_VIEW, 1025 Uri.parse(webAddress.toString())); 1026 } catch (ParseException e) { 1027 Log.e(TAG, "Couldn't parse website: " + entry.data); 1028 } 1029 mOtherEntries.add(entry); 1030 } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 1031 // Build SipAddress entries 1032 entry.uri = null; 1033 entry.maxLines = 1; 1034 entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 1035 Uri.fromParts(Constants.SCHEME_SIP, entry.data, null)); 1036 mOtherEntries.add(entry); 1037 // TODO: Consider moving the SipAddress into its own 1038 // section (rather than lumping it in with mOtherEntries) 1039 // so that we can reposition it right under the phone number. 1040 // (Then, we'd also update FallbackSource.java to set 1041 // secondary=false for this field, and tweak the weight 1042 // of its DataKind.) 1043 } else { 1044 // Handle showing custom rows 1045 entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri); 1046 1047 // Use social summary when requested by external source 1048 final DataStatus status = mStatuses.get(entry.id); 1049 final boolean hasSocial = kind.actionBodySocial && status != null; 1050 if (hasSocial) { 1051 entry.applyStatus(status, true); 1052 } 1053 1054 if (hasSocial || hasData) { 1055 mOtherEntries.add(entry); 1056 } 1057 } 1058 } 1059 } 1060 } 1061 } 1062 1063 static String buildActionString(DataKind kind, ContentValues values, boolean lowerCase, 1064 Context context) { 1065 if (kind.actionHeader == null) { 1066 return null; 1067 } 1068 CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values); 1069 if (actionHeader == null) { 1070 return null; 1071 } 1072 return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString(); 1073 } 1074 1075 static String buildDataString(DataKind kind, ContentValues values, Context context) { 1076 if (kind.actionBody == null) { 1077 return null; 1078 } 1079 CharSequence actionBody = kind.actionBody.inflateUsing(context, values); 1080 return actionBody == null ? null : actionBody.toString(); 1081 } 1082 1083 /** 1084 * A basic structure with the data for a contact entry in the list. 1085 */ 1086 static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> { 1087 public Context context = null; 1088 public String resPackageName = null; 1089 public int actionIcon = -1; 1090 public boolean isPrimary = false; 1091 public int secondaryActionIcon = -1; 1092 public Intent intent; 1093 public Intent secondaryIntent = null; 1094 public int maxLabelLines = 1; 1095 public ArrayList<Long> ids = new ArrayList<Long>(); 1096 public int collapseCount = 0; 1097 1098 public int presence = -1; 1099 1100 public CharSequence footerLine = null; 1101 1102 private ViewEntry() { 1103 } 1104 1105 /** 1106 * Build new {@link ViewEntry} and populate from the given values. 1107 */ 1108 public static ViewEntry fromValues(Context context, String mimeType, DataKind kind, 1109 long rawContactId, long dataId, ContentValues values) { 1110 final ViewEntry entry = new ViewEntry(); 1111 entry.context = context; 1112 entry.contactId = rawContactId; 1113 entry.id = dataId; 1114 entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id); 1115 entry.mimetype = mimeType; 1116 entry.label = buildActionString(kind, values, false, context); 1117 entry.data = buildDataString(kind, values, context); 1118 1119 if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) { 1120 entry.type = values.getAsInteger(kind.typeColumn); 1121 } 1122 if (kind.iconRes > 0) { 1123 entry.resPackageName = kind.resPackageName; 1124 entry.actionIcon = kind.iconRes; 1125 } 1126 1127 return entry; 1128 } 1129 1130 /** 1131 * Apply given {@link DataStatus} values over this {@link ViewEntry} 1132 * 1133 * @param fillData When true, the given status replaces {@link #data} 1134 * and {@link #footerLine}. Otherwise only {@link #presence} 1135 * is updated. 1136 */ 1137 public ViewEntry applyStatus(DataStatus status, boolean fillData) { 1138 presence = status.getPresence(); 1139 if (fillData && status.isValid()) { 1140 this.data = status.getStatus().toString(); 1141 this.footerLine = status.getTimestampLabel(context); 1142 } 1143 1144 return this; 1145 } 1146 1147 public boolean collapseWith(ViewEntry entry) { 1148 // assert equal collapse keys 1149 if (!shouldCollapseWith(entry)) { 1150 return false; 1151 } 1152 1153 // Choose the label associated with the highest type precedence. 1154 if (TypePrecedence.getTypePrecedence(mimetype, type) 1155 > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) { 1156 type = entry.type; 1157 label = entry.label; 1158 } 1159 1160 // Choose the max of the maxLines and maxLabelLines values. 1161 maxLines = Math.max(maxLines, entry.maxLines); 1162 maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines); 1163 1164 // Choose the presence with the highest precedence. 1165 if (StatusUpdates.getPresencePrecedence(presence) 1166 < StatusUpdates.getPresencePrecedence(entry.presence)) { 1167 presence = entry.presence; 1168 } 1169 1170 // If any of the collapsed entries are primary make the whole thing primary. 1171 isPrimary = entry.isPrimary ? true : isPrimary; 1172 1173 // uri, and contactdId, shouldn't make a difference. Just keep the original. 1174 1175 // Keep track of all the ids that have been collapsed with this one. 1176 ids.add(entry.id); 1177 collapseCount++; 1178 return true; 1179 } 1180 1181 public boolean shouldCollapseWith(ViewEntry entry) { 1182 if (entry == null) { 1183 return false; 1184 } 1185 1186 if (!ContactsUtils.shouldCollapse(context, mimetype, data, entry.mimetype, 1187 entry.data)) { 1188 return false; 1189 } 1190 1191 if (!TextUtils.equals(mimetype, entry.mimetype) 1192 || !ContactsUtils.areIntentActionEqual(intent, entry.intent) 1193 || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent) 1194 || actionIcon != entry.actionIcon) { 1195 return false; 1196 } 1197 1198 return true; 1199 } 1200 } 1201 1202 /** Cache of the children views of a row */ 1203 static class ViewCache { 1204 public TextView label; 1205 public TextView data; 1206 public TextView footer; 1207 public ImageView actionIcon; 1208 public ImageView presenceIcon; 1209 public ImageView primaryIcon; 1210 public ImageView secondaryActionButton; 1211 public View secondaryActionDivider; 1212 1213 // Need to keep track of this too 1214 ViewEntry entry; 1215 } 1216 1217 private final class ViewAdapter extends ContactEntryAdapter<ViewEntry> 1218 implements View.OnClickListener { 1219 1220 1221 ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) { 1222 super(context, sections, SHOW_SEPARATORS); 1223 } 1224 1225 public void onClick(View v) { 1226 Intent intent = (Intent) v.getTag(); 1227 startActivity(intent); 1228 } 1229 1230 @Override 1231 public View getView(int position, View convertView, ViewGroup parent) { 1232 ViewEntry entry = getEntry(mSections, position, false); 1233 View v; 1234 1235 ViewCache views; 1236 1237 // Check to see if we can reuse convertView 1238 if (convertView != null) { 1239 v = convertView; 1240 views = (ViewCache) v.getTag(); 1241 } else { 1242 // Create a new view if needed 1243 v = mInflater.inflate(R.layout.list_item_text_icons, parent, false); 1244 1245 // Cache the children 1246 views = new ViewCache(); 1247 views.label = (TextView) v.findViewById(android.R.id.text1); 1248 views.data = (TextView) v.findViewById(android.R.id.text2); 1249 views.footer = (TextView) v.findViewById(R.id.footer); 1250 views.actionIcon = (ImageView) v.findViewById(R.id.action_icon); 1251 views.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon); 1252 views.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon); 1253 views.secondaryActionButton = (ImageView) v.findViewById( 1254 R.id.secondary_action_button); 1255 views.secondaryActionButton.setOnClickListener(this); 1256 views.secondaryActionDivider = v.findViewById(R.id.divider); 1257 v.setTag(views); 1258 } 1259 1260 // Update the entry in the view cache 1261 views.entry = entry; 1262 1263 // Bind the data to the view 1264 bindView(v, entry); 1265 return v; 1266 } 1267 1268 @Override 1269 protected View newView(int position, ViewGroup parent) { 1270 // getView() handles this 1271 throw new UnsupportedOperationException(); 1272 } 1273 1274 @Override 1275 protected void bindView(View view, ViewEntry entry) { 1276 final Resources resources = mContext.getResources(); 1277 ViewCache views = (ViewCache) view.getTag(); 1278 1279 // Set the label 1280 TextView label = views.label; 1281 setMaxLines(label, entry.maxLabelLines); 1282 label.setText(entry.label); 1283 1284 // Set the data 1285 TextView data = views.data; 1286 if (data != null) { 1287 if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE) 1288 || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) { 1289 data.setText(PhoneNumberUtils.formatNumber(entry.data)); 1290 } else { 1291 data.setText(entry.data); 1292 } 1293 setMaxLines(data, entry.maxLines); 1294 } 1295 1296 // Set the footer 1297 if (!TextUtils.isEmpty(entry.footerLine)) { 1298 views.footer.setText(entry.footerLine); 1299 views.footer.setVisibility(View.VISIBLE); 1300 } else { 1301 views.footer.setVisibility(View.GONE); 1302 } 1303 1304 // Set the primary icon 1305 views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE); 1306 1307 // Set the action icon 1308 ImageView action = views.actionIcon; 1309 if (entry.actionIcon != -1) { 1310 Drawable actionIcon; 1311 if (entry.resPackageName != null) { 1312 // Load external resources through PackageManager 1313 actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName, 1314 entry.actionIcon, null); 1315 } else { 1316 actionIcon = resources.getDrawable(entry.actionIcon); 1317 } 1318 action.setImageDrawable(actionIcon); 1319 action.setVisibility(View.VISIBLE); 1320 } else { 1321 // Things should still line up as if there was an icon, so make it invisible 1322 action.setVisibility(View.INVISIBLE); 1323 } 1324 1325 // Set the presence icon 1326 Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon( 1327 mContext, entry.presence); 1328 ImageView presenceIconView = views.presenceIcon; 1329 if (presenceIcon != null) { 1330 presenceIconView.setImageDrawable(presenceIcon); 1331 presenceIconView.setVisibility(View.VISIBLE); 1332 } else { 1333 presenceIconView.setVisibility(View.GONE); 1334 } 1335 1336 // Set the secondary action button 1337 ImageView secondaryActionView = views.secondaryActionButton; 1338 Drawable secondaryActionIcon = null; 1339 if (entry.secondaryActionIcon != -1) { 1340 secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon); 1341 } 1342 if (entry.secondaryIntent != null && secondaryActionIcon != null) { 1343 secondaryActionView.setImageDrawable(secondaryActionIcon); 1344 secondaryActionView.setTag(entry.secondaryIntent); 1345 secondaryActionView.setVisibility(View.VISIBLE); 1346 views.secondaryActionDivider.setVisibility(View.VISIBLE); 1347 } else { 1348 secondaryActionView.setVisibility(View.GONE); 1349 views.secondaryActionDivider.setVisibility(View.GONE); 1350 } 1351 } 1352 1353 private void setMaxLines(TextView textView, int maxLines) { 1354 if (maxLines == 1) { 1355 textView.setSingleLine(true); 1356 textView.setEllipsize(TextUtils.TruncateAt.END); 1357 } else { 1358 textView.setSingleLine(false); 1359 textView.setMaxLines(maxLines); 1360 textView.setEllipsize(null); 1361 } 1362 } 1363 } 1364 1365 private interface StatusQuery { 1366 final String[] PROJECTION = new String[] { 1367 Data._ID, 1368 Data.STATUS, 1369 Data.STATUS_RES_PACKAGE, 1370 Data.STATUS_ICON, 1371 Data.STATUS_LABEL, 1372 Data.STATUS_TIMESTAMP, 1373 Data.PRESENCE, 1374 }; 1375 1376 final int _ID = 0; 1377 } 1378 1379 @Override 1380 public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, 1381 boolean globalSearch) { 1382 if (globalSearch) { 1383 super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); 1384 } else { 1385 ContactsSearchManager.startSearch(this, initialQuery); 1386 } 1387 } 1388 } 1389