• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
onClick(DialogInterface dialog, int which)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
onCreate(Bundle icicle)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
onResume()251     protected void onResume() {
252         super.onResume();
253         startEntityQuery();
254     }
255 
256     @Override
onPause()257     protected void onPause() {
258         super.onPause();
259         closeCursor();
260     }
261 
262     @Override
onDestroy()263     protected void onDestroy() {
264         super.onDestroy();
265         closeCursor();
266     }
267 
268     @Override
onCreateDialog(int id)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} */
onQueryComplete(int token, Object cookie, final Cursor cursor)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 
getRefreshedContactId()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      */
readStatuses(Cursor cursor)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 
setupContactCursor(ContentResolver resolver, Uri lookupUri)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 
startEntityQuery()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 
closeCursor()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      */
considerBindData()504     private void considerBindData() {
505         if (mHasEntities && mHasStatuses) {
506             bindData();
507         }
508     }
509 
bindData()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
onCreateOptionsMenu(Menu menu)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
onPrepareOptionsMenu(Menu menu)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
onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)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
onOptionsItemSelected(MenuItem item)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
onContextItemSelected(MenuItem item)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 
makeItemDefault(MenuItem item)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      */
showJoinAggregateActivity()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
onActivityResult(int requestCode, int resultCode, Intent intent)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 
joinAggregate(final long contactId)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      */
setAggregationException(long rawContactId, int exceptionType)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 
showOptionsActivity()756     private void showOptionsActivity() {
757         final Intent intent = new Intent(this, ContactOptionsActivity.class);
758         intent.setData(mLookupUri);
759         startActivity(intent);
760     }
761 
getViewEntryForMenuItem(MenuItem item)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
onKeyDown(int keyCode, KeyEvent event)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 
onItemClick(AdapterView parent, View v, int position, long id)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      */
signalError()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      */
buildEntries()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 
buildActionString(DataKind kind, ContentValues values, boolean lowerCase, Context context)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 
buildDataString(DataKind kind, ContentValues values, Context context)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 
ViewEntry()1102         private ViewEntry() {
1103         }
1104 
1105         /**
1106          * Build new {@link ViewEntry} and populate from the given values.
1107          */
fromValues(Context context, String mimeType, DataKind kind, long rawContactId, long dataId, ContentValues values)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          */
applyStatus(DataStatus status, boolean fillData)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 
collapseWith(ViewEntry entry)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 
shouldCollapseWith(ViewEntry entry)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 
ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections)1221         ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
1222             super(context, sections, SHOW_SEPARATORS);
1223         }
1224 
onClick(View v)1225         public void onClick(View v) {
1226             Intent intent = (Intent) v.getTag();
1227             startActivity(intent);
1228         }
1229 
1230         @Override
getView(int position, View convertView, ViewGroup parent)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
newView(int position, ViewGroup parent)1269         protected View newView(int position, ViewGroup parent) {
1270             // getView() handles this
1271             throw new UnsupportedOperationException();
1272         }
1273 
1274         @Override
bindView(View view, ViewEntry entry)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 
setMaxLines(TextView textView, int maxLines)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
startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch)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