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