Home | History | Annotate | Download | only in contacts
      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