| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.contacts; |
| |
| import com.android.contacts.TextHighlightingAnimation.TextWithHighlighting; |
| import com.android.contacts.model.ContactsSource; |
| import com.android.contacts.model.Sources; |
| import com.android.contacts.ui.ContactsPreferences; |
| import com.android.contacts.ui.ContactsPreferencesActivity; |
| import com.android.contacts.ui.ContactsPreferencesActivity.Prefs; |
| import com.android.contacts.util.AccountSelectionUtil; |
| import com.android.contacts.util.Constants; |
| |
| import android.accounts.Account; |
| import android.accounts.AccountManager; |
| import android.app.Activity; |
| import android.app.AlertDialog; |
| import android.app.Dialog; |
| import android.app.ListActivity; |
| import android.app.SearchManager; |
| import android.content.AsyncQueryHandler; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.IContentService; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.content.UriMatcher; |
| import android.content.res.ColorStateList; |
| import android.content.res.Resources; |
| import android.database.CharArrayBuffer; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.database.MatrixCursor; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.graphics.Typeface; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.net.Uri; |
| import android.net.Uri.Builder; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Parcelable; |
| import android.os.RemoteException; |
| import android.preference.PreferenceManager; |
| import android.provider.ContactsContract; |
| import android.provider.Settings; |
| import android.provider.Contacts.ContactMethods; |
| import android.provider.Contacts.People; |
| import android.provider.Contacts.PeopleColumns; |
| import android.provider.Contacts.Phones; |
| import android.provider.ContactsContract.ContactCounts; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.Data; |
| import android.provider.ContactsContract.Intents; |
| import android.provider.ContactsContract.ProviderStatus; |
| import android.provider.ContactsContract.RawContacts; |
| import android.provider.ContactsContract.SearchSnippetColumns; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Nickname; |
| import android.provider.ContactsContract.CommonDataKinds.Organization; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.CommonDataKinds.Photo; |
| import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; |
| import android.provider.ContactsContract.Contacts.AggregationSuggestions; |
| import android.provider.ContactsContract.Intents.Insert; |
| import android.provider.ContactsContract.Intents.UI; |
| import android.telephony.TelephonyManager; |
| import android.text.Editable; |
| import android.text.Html; |
| import android.text.TextUtils; |
| import android.text.TextWatcher; |
| import android.util.Log; |
| import android.view.ContextMenu; |
| import android.view.ContextThemeWrapper; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ContextMenu.ContextMenuInfo; |
| import android.view.View.OnClickListener; |
| import android.view.View.OnFocusChangeListener; |
| import android.view.View.OnTouchListener; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputMethodManager; |
| import android.widget.AbsListView; |
| import android.widget.AdapterView; |
| import android.widget.ArrayAdapter; |
| import android.widget.Button; |
| import android.widget.CursorAdapter; |
| import android.widget.Filter; |
| import android.widget.ImageView; |
| import android.widget.ListView; |
| import android.widget.QuickContactBadge; |
| import android.widget.SectionIndexer; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| import android.widget.AbsListView.OnScrollListener; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Random; |
| |
| /** |
| * Displays a list of contacts. Usually is embedded into the ContactsActivity. |
| */ |
| @SuppressWarnings("deprecation") |
| public class ContactsListActivity extends ListActivity implements View.OnCreateContextMenuListener, |
| View.OnClickListener, View.OnKeyListener, TextWatcher, TextView.OnEditorActionListener, |
| OnFocusChangeListener, OnTouchListener { |
| |
| public static class JoinContactActivity extends ContactsListActivity { |
| |
| } |
| |
| public static class ContactsSearchActivity extends ContactsListActivity { |
| |
| } |
| |
| private static final String TAG = "ContactsListActivity"; |
| |
| private static final boolean ENABLE_ACTION_ICON_OVERLAYS = true; |
| |
| private static final String LIST_STATE_KEY = "liststate"; |
| private static final String SHORTCUT_ACTION_KEY = "shortcutAction"; |
| |
| static final int MENU_ITEM_VIEW_CONTACT = 1; |
| static final int MENU_ITEM_CALL = 2; |
| static final int MENU_ITEM_EDIT_BEFORE_CALL = 3; |
| static final int MENU_ITEM_SEND_SMS = 4; |
| static final int MENU_ITEM_SEND_IM = 5; |
| static final int MENU_ITEM_EDIT = 6; |
| static final int MENU_ITEM_DELETE = 7; |
| static final int MENU_ITEM_TOGGLE_STAR = 8; |
| |
| private static final int SUBACTIVITY_NEW_CONTACT = 1; |
| private static final int SUBACTIVITY_VIEW_CONTACT = 2; |
| private static final int SUBACTIVITY_DISPLAY_GROUP = 3; |
| private static final int SUBACTIVITY_SEARCH = 4; |
| private static final int SUBACTIVITY_FILTER = 5; |
| |
| private static final int TEXT_HIGHLIGHTING_ANIMATION_DURATION = 350; |
| |
| /** |
| * The action for the join contact activity. |
| * <p> |
| * Input: extra field {@link #EXTRA_AGGREGATE_ID} is the aggregate ID. |
| * |
| * TODO: move to {@link ContactsContract}. |
| */ |
| public static final String JOIN_AGGREGATE = |
| "com.android.contacts.action.JOIN_AGGREGATE"; |
| |
| /** |
| * Used with {@link #JOIN_AGGREGATE} to give it the target for aggregation. |
| * <p> |
| * Type: LONG |
| */ |
| public static final String EXTRA_AGGREGATE_ID = |
| "com.android.contacts.action.AGGREGATE_ID"; |
| |
| /** |
| * Used with {@link #JOIN_AGGREGATE} to give it the name of the aggregation target. |
| * <p> |
| * Type: STRING |
| */ |
| @Deprecated |
| public static final String EXTRA_AGGREGATE_NAME = |
| "com.android.contacts.action.AGGREGATE_NAME"; |
| |
| public static final String AUTHORITIES_FILTER_KEY = "authorities"; |
| |
| private static final Uri CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS = |
| buildSectionIndexerUri(Contacts.CONTENT_URI); |
| |
| /** Mask for picker mode */ |
| static final int MODE_MASK_PICKER = 0x80000000; |
| /** Mask for no presence mode */ |
| static final int MODE_MASK_NO_PRESENCE = 0x40000000; |
| /** Mask for enabling list filtering */ |
| static final int MODE_MASK_NO_FILTER = 0x20000000; |
| /** Mask for having a "create new contact" header in the list */ |
| static final int MODE_MASK_CREATE_NEW = 0x10000000; |
| /** Mask for showing photos in the list */ |
| static final int MODE_MASK_SHOW_PHOTOS = 0x08000000; |
| /** Mask for hiding additional information e.g. primary phone number in the list */ |
| static final int MODE_MASK_NO_DATA = 0x04000000; |
| /** Mask for showing a call button in the list */ |
| static final int MODE_MASK_SHOW_CALL_BUTTON = 0x02000000; |
| /** Mask to disable quickcontact (images will show as normal images) */ |
| static final int MODE_MASK_DISABLE_QUIKCCONTACT = 0x01000000; |
| /** Mask to show the total number of contacts at the top */ |
| static final int MODE_MASK_SHOW_NUMBER_OF_CONTACTS = 0x00800000; |
| |
| /** Unknown mode */ |
| static final int MODE_UNKNOWN = 0; |
| /** Default mode */ |
| static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; |
| /** Custom mode */ |
| static final int MODE_CUSTOM = 8; |
| /** Show all starred contacts */ |
| static final int MODE_STARRED = 20 | MODE_MASK_SHOW_PHOTOS; |
| /** Show frequently contacted contacts */ |
| static final int MODE_FREQUENT = 30 | MODE_MASK_SHOW_PHOTOS; |
| /** Show starred and the frequent */ |
| static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_CALL_BUTTON; |
| /** Show all contacts and pick them when clicking */ |
| static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS |
| | MODE_MASK_DISABLE_QUIKCCONTACT; |
| /** Show all contacts as well as the option to create a new one */ |
| static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW |
| | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT; |
| /** Show all people through the legacy provider and pick them when clicking */ |
| static final int MODE_LEGACY_PICK_PERSON = 43 | MODE_MASK_PICKER |
| | MODE_MASK_DISABLE_QUIKCCONTACT; |
| /** Show all people through the legacy provider as well as the option to create a new one */ |
| static final int MODE_LEGACY_PICK_OR_CREATE_PERSON = 44 | MODE_MASK_PICKER |
| | MODE_MASK_CREATE_NEW | MODE_MASK_DISABLE_QUIKCCONTACT; |
| /** Show all contacts and pick them when clicking, and allow creating a new contact */ |
| static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW |
| | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT; |
| /** Show all phone numbers and pick them when clicking */ |
| static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE; |
| /** Show all phone numbers through the legacy provider and pick them when clicking */ |
| static final int MODE_LEGACY_PICK_PHONE = |
| 51 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER; |
| /** Show all postal addresses and pick them when clicking */ |
| static final int MODE_PICK_POSTAL = |
| 55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER; |
| /** Show all postal addresses and pick them when clicking */ |
| static final int MODE_LEGACY_PICK_POSTAL = |
| 56 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER; |
| static final int MODE_GROUP = 57 | MODE_MASK_SHOW_PHOTOS; |
| /** Run a search query */ |
| static final int MODE_QUERY = 60 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_NO_FILTER |
| | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; |
| /** Run a search query in PICK mode, but that still launches to VIEW */ |
| static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_PICKER |
| | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; |
| |
| /** Show join suggestions followed by an A-Z list */ |
| static final int MODE_JOIN_CONTACT = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE |
| | MODE_MASK_NO_DATA | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT; |
| |
| /** Run a search query in a PICK mode */ |
| static final int MODE_QUERY_PICK = 75 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_NO_FILTER |
| | MODE_MASK_PICKER | MODE_MASK_DISABLE_QUIKCCONTACT | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; |
| |
| /** Run a search query in a PICK_PHONE mode */ |
| static final int MODE_QUERY_PICK_PHONE = 80 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER |
| | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; |
| |
| /** Run a search query in PICK mode, but that still launches to EDIT */ |
| static final int MODE_QUERY_PICK_TO_EDIT = 85 | MODE_MASK_NO_FILTER | MODE_MASK_SHOW_PHOTOS |
| | MODE_MASK_PICKER | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; |
| |
| /** |
| * An action used to do perform search while in a contact picker. It is initiated |
| * by the ContactListActivity itself. |
| */ |
| private static final String ACTION_SEARCH_INTERNAL = "com.android.contacts.INTERNAL_SEARCH"; |
| |
| /** Maximum number of suggestions shown for joining aggregates */ |
| static final int MAX_SUGGESTIONS = 4; |
| |
| static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] { |
| Contacts._ID, // 0 |
| Contacts.DISPLAY_NAME_PRIMARY, // 1 |
| Contacts.DISPLAY_NAME_ALTERNATIVE, // 2 |
| Contacts.SORT_KEY_PRIMARY, // 3 |
| Contacts.STARRED, // 4 |
| Contacts.TIMES_CONTACTED, // 5 |
| Contacts.CONTACT_PRESENCE, // 6 |
| Contacts.PHOTO_ID, // 7 |
| Contacts.LOOKUP_KEY, // 8 |
| Contacts.PHONETIC_NAME, // 9 |
| Contacts.HAS_PHONE_NUMBER, // 10 |
| }; |
| static final String[] CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL = new String[] { |
| Contacts._ID, // 0 |
| Contacts.DISPLAY_NAME_PRIMARY, // 1 |
| Contacts.DISPLAY_NAME_ALTERNATIVE, // 2 |
| Contacts.SORT_KEY_PRIMARY, // 3 |
| Contacts.STARRED, // 4 |
| Contacts.TIMES_CONTACTED, // 5 |
| Contacts.CONTACT_PRESENCE, // 6 |
| Contacts.PHOTO_ID, // 7 |
| Contacts.LOOKUP_KEY, // 8 |
| Contacts.PHONETIC_NAME, // 9 |
| // email lookup doesn't included HAS_PHONE_NUMBER in projection |
| }; |
| |
| static final String[] CONTACTS_SUMMARY_FILTER_PROJECTION = new String[] { |
| Contacts._ID, // 0 |
| Contacts.DISPLAY_NAME_PRIMARY, // 1 |
| Contacts.DISPLAY_NAME_ALTERNATIVE, // 2 |
| Contacts.SORT_KEY_PRIMARY, // 3 |
| Contacts.STARRED, // 4 |
| Contacts.TIMES_CONTACTED, // 5 |
| Contacts.CONTACT_PRESENCE, // 6 |
| Contacts.PHOTO_ID, // 7 |
| Contacts.LOOKUP_KEY, // 8 |
| Contacts.PHONETIC_NAME, // 9 |
| Contacts.HAS_PHONE_NUMBER, // 10 |
| SearchSnippetColumns.SNIPPET_MIMETYPE, // 11 |
| SearchSnippetColumns.SNIPPET_DATA1, // 12 |
| SearchSnippetColumns.SNIPPET_DATA4, // 13 |
| }; |
| |
| static final String[] LEGACY_PEOPLE_PROJECTION = new String[] { |
| People._ID, // 0 |
| People.DISPLAY_NAME, // 1 |
| People.DISPLAY_NAME, // 2 |
| People.DISPLAY_NAME, // 3 |
| People.STARRED, // 4 |
| PeopleColumns.TIMES_CONTACTED, // 5 |
| People.PRESENCE_STATUS, // 6 |
| }; |
| static final int SUMMARY_ID_COLUMN_INDEX = 0; |
| static final int SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1; |
| static final int SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2; |
| static final int SUMMARY_SORT_KEY_PRIMARY_COLUMN_INDEX = 3; |
| static final int SUMMARY_STARRED_COLUMN_INDEX = 4; |
| static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 5; |
| static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 6; |
| static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 7; |
| static final int SUMMARY_LOOKUP_KEY_COLUMN_INDEX = 8; |
| static final int SUMMARY_PHONETIC_NAME_COLUMN_INDEX = 9; |
| static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 10; |
| static final int SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX = 11; |
| static final int SUMMARY_SNIPPET_DATA1_COLUMN_INDEX = 12; |
| static final int SUMMARY_SNIPPET_DATA4_COLUMN_INDEX = 13; |
| |
| static final String[] PHONES_PROJECTION = new String[] { |
| Phone._ID, //0 |
| Phone.TYPE, //1 |
| Phone.LABEL, //2 |
| Phone.NUMBER, //3 |
| Phone.DISPLAY_NAME, // 4 |
| Phone.CONTACT_ID, // 5 |
| }; |
| static final String[] LEGACY_PHONES_PROJECTION = new String[] { |
| Phones._ID, //0 |
| Phones.TYPE, //1 |
| Phones.LABEL, //2 |
| Phones.NUMBER, //3 |
| People.DISPLAY_NAME, // 4 |
| }; |
| static final int PHONE_ID_COLUMN_INDEX = 0; |
| static final int PHONE_TYPE_COLUMN_INDEX = 1; |
| static final int PHONE_LABEL_COLUMN_INDEX = 2; |
| static final int PHONE_NUMBER_COLUMN_INDEX = 3; |
| static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 4; |
| static final int PHONE_CONTACT_ID_COLUMN_INDEX = 5; |
| |
| static final String[] POSTALS_PROJECTION = new String[] { |
| StructuredPostal._ID, //0 |
| StructuredPostal.TYPE, //1 |
| StructuredPostal.LABEL, //2 |
| StructuredPostal.DATA, //3 |
| StructuredPostal.DISPLAY_NAME, // 4 |
| }; |
| static final String[] LEGACY_POSTALS_PROJECTION = new String[] { |
| ContactMethods._ID, //0 |
| ContactMethods.TYPE, //1 |
| ContactMethods.LABEL, //2 |
| ContactMethods.DATA, //3 |
| People.DISPLAY_NAME, // 4 |
| }; |
| static final String[] RAW_CONTACTS_PROJECTION = new String[] { |
| RawContacts._ID, //0 |
| RawContacts.CONTACT_ID, //1 |
| RawContacts.ACCOUNT_TYPE, //2 |
| }; |
| |
| static final int POSTAL_ID_COLUMN_INDEX = 0; |
| static final int POSTAL_TYPE_COLUMN_INDEX = 1; |
| static final int POSTAL_LABEL_COLUMN_INDEX = 2; |
| static final int POSTAL_ADDRESS_COLUMN_INDEX = 3; |
| static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4; |
| |
| private static final int QUERY_TOKEN = 42; |
| |
| static final String KEY_PICKER_MODE = "picker_mode"; |
| |
| private ContactItemListAdapter mAdapter; |
| |
| int mMode = MODE_DEFAULT; |
| |
| private QueryHandler mQueryHandler; |
| private boolean mJustCreated; |
| private boolean mSyncEnabled; |
| Uri mSelectedContactUri; |
| |
| // private boolean mDisplayAll; |
| private boolean mDisplayOnlyPhones; |
| |
| private Uri mGroupUri; |
| |
| private long mQueryAggregateId; |
| |
| private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>(); |
| private int mWritableSourcesCnt; |
| private int mReadOnlySourcesCnt; |
| |
| /** |
| * Used to keep track of the scroll state of the list. |
| */ |
| private Parcelable mListState = null; |
| |
| private String mShortcutAction; |
| |
| /** |
| * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. |
| */ |
| private int mQueryMode = QUERY_MODE_NONE; |
| |
| private static final int QUERY_MODE_NONE = -1; |
| private static final int QUERY_MODE_MAILTO = 1; |
| private static final int QUERY_MODE_TEL = 2; |
| |
| private int mProviderStatus = ProviderStatus.STATUS_NORMAL; |
| |
| private boolean mSearchMode; |
| private boolean mSearchResultsMode; |
| private boolean mShowNumberOfContacts; |
| |
| private boolean mShowSearchSnippets; |
| private boolean mSearchInitiated; |
| |
| private String mInitialFilter; |
| |
| private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1"; |
| private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1"; |
| |
| /** |
| * In the {@link #MODE_JOIN_CONTACT} determines whether we display a list item with the label |
| * "Show all contacts" or actually show all contacts |
| */ |
| private boolean mJoinModeShowAllContacts; |
| |
| /** |
| * The ID of the special item described above. |
| */ |
| private static final long JOIN_MODE_SHOW_ALL_CONTACTS_ID = -2; |
| |
| // Uri matcher for contact id |
| private static final int CONTACTS_ID = 1001; |
| private static final UriMatcher sContactsIdMatcher; |
| |
| private ContactPhotoLoader mPhotoLoader; |
| |
| final String[] sLookupProjection = new String[] { |
| Contacts.LOOKUP_KEY |
| }; |
| |
| static { |
| sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH); |
| sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); |
| } |
| |
| private class DeleteClickListener implements DialogInterface.OnClickListener { |
| public void onClick(DialogInterface dialog, int which) { |
| if (mSelectedContactUri != null) { |
| getContentResolver().delete(mSelectedContactUri, null, null); |
| } |
| } |
| } |
| |
| /** |
| * A {@link TextHighlightingAnimation} that redraws just the contact display name in a |
| * list item. |
| */ |
| private static class NameHighlightingAnimation extends TextHighlightingAnimation { |
| private final ListView mListView; |
| |
| private NameHighlightingAnimation(ListView listView, int duration) { |
| super(duration); |
| this.mListView = listView; |
| } |
| |
| /** |
| * Redraws all visible items of the list corresponding to contacts |
| */ |
| @Override |
| protected void invalidate() { |
| int childCount = mListView.getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| View itemView = mListView.getChildAt(i); |
| if (itemView instanceof ContactListItemView) { |
| final ContactListItemView view = (ContactListItemView)itemView; |
| view.getNameTextView().invalidate(); |
| } |
| } |
| } |
| |
| @Override |
| protected void onAnimationStarted() { |
| mListView.setScrollingCacheEnabled(false); |
| } |
| |
| @Override |
| protected void onAnimationEnded() { |
| mListView.setScrollingCacheEnabled(true); |
| } |
| } |
| |
| // The size of a home screen shortcut icon. |
| private int mIconSize; |
| private ContactsPreferences mContactsPrefs; |
| private int mDisplayOrder; |
| private int mSortOrder; |
| private boolean mHighlightWhenScrolling; |
| private TextHighlightingAnimation mHighlightingAnimation; |
| private SearchEditText mSearchEditText; |
| |
| /** |
| * An approximation of the background color of the pinned header. This color |
| * is used when the pinned header is being pushed up. At that point the header |
| * "fades away". Rather than computing a faded bitmap based on the 9-patch |
| * normally used for the background, we will use a solid color, which will |
| * provide better performance and reduced complexity. |
| */ |
| private int mPinnedHeaderBackgroundColor; |
| |
| private ContentObserver mProviderStatusObserver = new ContentObserver(new Handler()) { |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| checkProviderState(true); |
| } |
| }; |
| |
| @Override |
| protected void onCreate(Bundle icicle) { |
| super.onCreate(icicle); |
| |
| mIconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); |
| mContactsPrefs = new ContactsPreferences(this); |
| mPhotoLoader = new ContactPhotoLoader(this, R.drawable.ic_contact_list_picture); |
| |
| // Resolve the intent |
| final Intent intent = getIntent(); |
| |
| // Allow the title to be set to a custom String using an extra on the intent |
| String title = intent.getStringExtra(UI.TITLE_EXTRA_KEY); |
| if (title != null) { |
| setTitle(title); |
| } |
| |
| String action = intent.getAction(); |
| String component = intent.getComponent().getClassName(); |
| |
| // When we get a FILTER_CONTACTS_ACTION, it represents search in the context |
| // of some other action. Let's retrieve the original action to provide proper |
| // context for the search queries. |
| if (UI.FILTER_CONTACTS_ACTION.equals(action)) { |
| mSearchMode = true; |
| mShowSearchSnippets = true; |
| Bundle extras = intent.getExtras(); |
| if (extras != null) { |
| mInitialFilter = extras.getString(UI.FILTER_TEXT_EXTRA_KEY); |
| String originalAction = |
| extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY); |
| if (originalAction != null) { |
| action = originalAction; |
| } |
| String originalComponent = |
| extras.getString(ContactsSearchManager.ORIGINAL_COMPONENT_EXTRA_KEY); |
| if (originalComponent != null) { |
| component = originalComponent; |
| } |
| } else { |
| mInitialFilter = null; |
| } |
| } |
| |
| Log.i(TAG, "Called with action: " + action); |
| mMode = MODE_UNKNOWN; |
| if (UI.LIST_DEFAULT.equals(action) || UI.FILTER_CONTACTS_ACTION.equals(action)) { |
| mMode = MODE_DEFAULT; |
| // When mDefaultMode is true the mode is set in onResume(), since the preferneces |
| // activity may change it whenever this activity isn't running |
| } else if (UI.LIST_GROUP_ACTION.equals(action)) { |
| mMode = MODE_GROUP; |
| String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY); |
| if (TextUtils.isEmpty(groupName)) { |
| finish(); |
| return; |
| } |
| buildUserGroupUri(groupName); |
| } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) { |
| mMode = MODE_CUSTOM; |
| mDisplayOnlyPhones = false; |
| } else if (UI.LIST_STARRED_ACTION.equals(action)) { |
| mMode = mSearchMode ? MODE_DEFAULT : MODE_STARRED; |
| } else if (UI.LIST_FREQUENT_ACTION.equals(action)) { |
| mMode = mSearchMode ? MODE_DEFAULT : MODE_FREQUENT; |
| } else if (UI.LIST_STREQUENT_ACTION.equals(action)) { |
| mMode = mSearchMode ? MODE_DEFAULT : MODE_STREQUENT; |
| } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) { |
| mMode = MODE_CUSTOM; |
| mDisplayOnlyPhones = true; |
| } else if (Intent.ACTION_PICK.equals(action)) { |
| // XXX These should be showing the data from the URI given in |
| // the Intent. |
| final String type = intent.resolveType(this); |
| if (Contacts.CONTENT_TYPE.equals(type)) { |
| mMode = MODE_PICK_CONTACT; |
| } else if (People.CONTENT_TYPE.equals(type)) { |
| mMode = MODE_LEGACY_PICK_PERSON; |
| } else if (Phone.CONTENT_TYPE.equals(type)) { |
| mMode = MODE_PICK_PHONE; |
| } else if (Phones.CONTENT_TYPE.equals(type)) { |
| mMode = MODE_LEGACY_PICK_PHONE; |
| } else if (StructuredPostal.CONTENT_TYPE.equals(type)) { |
| mMode = MODE_PICK_POSTAL; |
| } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) { |
| mMode = MODE_LEGACY_PICK_POSTAL; |
| } |
| } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) { |
| if (component.equals("alias.DialShortcut")) { |
| mMode = MODE_PICK_PHONE; |
| mShortcutAction = Intent.ACTION_CALL; |
| mShowSearchSnippets = false; |
| setTitle(R.string.callShortcutActivityTitle); |
| } else if (component.equals("alias.MessageShortcut")) { |
| mMode = MODE_PICK_PHONE; |
| mShortcutAction = Intent.ACTION_SENDTO; |
| mShowSearchSnippets = false; |
| setTitle(R.string.messageShortcutActivityTitle); |
| } else if (mSearchMode) { |
| mMode = MODE_PICK_CONTACT; |
| mShortcutAction = Intent.ACTION_VIEW; |
| setTitle(R.string.shortcutActivityTitle); |
| } else { |
| mMode = MODE_PICK_OR_CREATE_CONTACT; |
| mShortcutAction = Intent.ACTION_VIEW; |
| setTitle(R.string.shortcutActivityTitle); |
| } |
| } else if (Intent.ACTION_GET_CONTENT.equals(action)) { |
| final String type = intent.resolveType(this); |
| if (Contacts.CONTENT_ITEM_TYPE.equals(type)) { |
| if (mSearchMode) { |
| mMode = MODE_PICK_CONTACT; |
| } else { |
| mMode = MODE_PICK_OR_CREATE_CONTACT; |
| } |
| } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) { |
| mMode = MODE_PICK_PHONE; |
| } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) { |
| mMode = MODE_LEGACY_PICK_PHONE; |
| } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) { |
| mMode = MODE_PICK_POSTAL; |
| } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) { |
| mMode = MODE_LEGACY_PICK_POSTAL; |
| } else if (People.CONTENT_ITEM_TYPE.equals(type)) { |
| if (mSearchMode) { |
| mMode = MODE_LEGACY_PICK_PERSON; |
| } else { |
| mMode = MODE_LEGACY_PICK_OR_CREATE_PERSON; |
| } |
| } |
| |
| } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) { |
| mMode = MODE_INSERT_OR_EDIT_CONTACT; |
| } else if (Intent.ACTION_SEARCH.equals(action)) { |
| // See if the suggestion was clicked with a search action key (call button) |
| if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) { |
| String query = intent.getStringExtra(SearchManager.QUERY); |
| if (!TextUtils.isEmpty(query)) { |
| Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, |
| Uri.fromParts("tel", query, null)); |
| startActivity(newIntent); |
| } |
| finish(); |
| return; |
| } |
| |
| // See if search request has extras to specify query |
| if (intent.hasExtra(Insert.EMAIL)) { |
| mMode = MODE_QUERY_PICK_TO_VIEW; |
| mQueryMode = QUERY_MODE_MAILTO; |
| mInitialFilter = intent.getStringExtra(Insert.EMAIL); |
| } else if (intent.hasExtra(Insert.PHONE)) { |
| mMode = MODE_QUERY_PICK_TO_VIEW; |
| mQueryMode = QUERY_MODE_TEL; |
| mInitialFilter = intent.getStringExtra(Insert.PHONE); |
| } else { |
| // Otherwise handle the more normal search case |
| mMode = MODE_QUERY; |
| mShowSearchSnippets = true; |
| mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY); |
| } |
| mSearchResultsMode = true; |
| } else if (ACTION_SEARCH_INTERNAL.equals(action)) { |
| String originalAction = null; |
| Bundle extras = intent.getExtras(); |
| if (extras != null) { |
| originalAction = extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY); |
| } |
| mShortcutAction = intent.getStringExtra(SHORTCUT_ACTION_KEY); |
| |
| if (Intent.ACTION_INSERT_OR_EDIT.equals(originalAction)) { |
| mMode = MODE_QUERY_PICK_TO_EDIT; |
| mShowSearchSnippets = true; |
| mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY); |
| } else if (mShortcutAction != null && intent.hasExtra(Insert.PHONE)) { |
| mMode = MODE_QUERY_PICK_PHONE; |
| mQueryMode = QUERY_MODE_TEL; |
| mInitialFilter = intent.getStringExtra(Insert.PHONE); |
| } else { |
| mMode = MODE_QUERY_PICK; |
| mQueryMode = QUERY_MODE_NONE; |
| mShowSearchSnippets = true; |
| mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY); |
| } |
| mSearchResultsMode = true; |
| // Since this is the filter activity it receives all intents |
| // dispatched from the SearchManager for security reasons |
| // so we need to re-dispatch from here to the intended target. |
| } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) { |
| Uri data = intent.getData(); |
| Uri telUri = null; |
| if (sContactsIdMatcher.match(data) == CONTACTS_ID) { |
| long contactId = Long.valueOf(data.getLastPathSegment()); |
| final Cursor cursor = queryPhoneNumbers(contactId); |
| if (cursor != null) { |
| if (cursor.getCount() == 1 && cursor.moveToFirst()) { |
| int phoneNumberIndex = cursor.getColumnIndex(Phone.NUMBER); |
| String phoneNumber = cursor.getString(phoneNumberIndex); |
| telUri = Uri.parse("tel:" + phoneNumber); |
| } |
| cursor.close(); |
| } |
| } |
| // See if the suggestion was clicked with a search action key (call button) |
| Intent newIntent; |
| if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG)) && telUri != null) { |
| newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, telUri); |
| } else { |
| newIntent = new Intent(Intent.ACTION_VIEW, data); |
| } |
| startActivity(newIntent); |
| finish(); |
| return; |
| } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) { |
| Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData()); |
| startActivity(newIntent); |
| finish(); |
| return; |
| } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) { |
| // TODO actually support this in EditContactActivity. |
| String number = intent.getData().getSchemeSpecificPart(); |
| Intent newIntent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); |
| newIntent.putExtra(Intents.Insert.PHONE, number); |
| startActivity(newIntent); |
| finish(); |
| return; |
| } |
| |
| if (JOIN_AGGREGATE.equals(action)) { |
| if (mSearchMode) { |
| mMode = MODE_PICK_CONTACT; |
| } else { |
| mMode = MODE_JOIN_CONTACT; |
| mQueryAggregateId = intent.getLongExtra(EXTRA_AGGREGATE_ID, -1); |
| if (mQueryAggregateId == -1) { |
| Log.e(TAG, "Intent " + action + " is missing required extra: " |
| + EXTRA_AGGREGATE_ID); |
| setResult(RESULT_CANCELED); |
| finish(); |
| } |
| } |
| } |
| |
| if (mMode == MODE_UNKNOWN) { |
| mMode = MODE_DEFAULT; |
| } |
| |
| if (((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0 || mSearchMode) |
| && !mSearchResultsMode) { |
| mShowNumberOfContacts = true; |
| } |
| |
| if (mMode == MODE_JOIN_CONTACT) { |
| setContentView(R.layout.contacts_list_content_join); |
| TextView blurbView = (TextView)findViewById(R.id.join_contact_blurb); |
| |
| String blurb = getString(R.string.blurbJoinContactDataWith, |
| getContactDisplayName(mQueryAggregateId)); |
| blurbView.setText(blurb); |
| mJoinModeShowAllContacts = true; |
| } else if (mSearchMode) { |
| setContentView(R.layout.contacts_search_content); |
| } else if (mSearchResultsMode) { |
| setContentView(R.layout.contacts_list_search_results); |
| TextView titleText = (TextView)findViewById(R.id.search_results_for); |
| titleText.setText(Html.fromHtml(getString(R.string.search_results_for, |
| "<b>" + mInitialFilter + "</b>"))); |
| } else { |
| setContentView(R.layout.contacts_list_content); |
| } |
| |
| setupListView(); |
| if (mSearchMode) { |
| setupSearchView(); |
| } |
| |
| mQueryHandler = new QueryHandler(this); |
| mJustCreated = true; |
| |
| mSyncEnabled = true; |
| } |
| |
| /** |
| * Register an observer for provider status changes - we will need to |
| * reflect them in the UI. |
| */ |
| private void registerProviderStatusObserver() { |
| getContentResolver().registerContentObserver(ProviderStatus.CONTENT_URI, |
| false, mProviderStatusObserver); |
| } |
| |
| /** |
| * Register an observer for provider status changes - we will need to |
| * reflect them in the UI. |
| */ |
| private void unregisterProviderStatusObserver() { |
| getContentResolver().unregisterContentObserver(mProviderStatusObserver); |
| } |
| |
| private void setupListView() { |
| final ListView list = getListView(); |
| final LayoutInflater inflater = getLayoutInflater(); |
| |
| mHighlightingAnimation = |
| new NameHighlightingAnimation(list, TEXT_HIGHLIGHTING_ANIMATION_DURATION); |
| |
| // Tell list view to not show dividers. We'll do it ourself so that we can *not* show |
| // them when an A-Z headers is visible. |
| list.setDividerHeight(0); |
| list.setOnCreateContextMenuListener(this); |
| |
| mAdapter = new ContactItemListAdapter(this); |
| setListAdapter(mAdapter); |
| |
| if (list instanceof PinnedHeaderListView && mAdapter.getDisplaySectionHeadersEnabled()) { |
| mPinnedHeaderBackgroundColor = |
| getResources().getColor(R.color.pinned_header_background); |
| PinnedHeaderListView pinnedHeaderList = (PinnedHeaderListView)list; |
| View pinnedHeader = inflater.inflate(R.layout.list_section, list, false); |
| pinnedHeaderList.setPinnedHeaderView(pinnedHeader); |
| } |
| |
| list.setOnScrollListener(mAdapter); |
| list.setOnKeyListener(this); |
| list.setOnFocusChangeListener(this); |
| list.setOnTouchListener(this); |
| |
| // We manually save/restore the listview state |
| list.setSaveEnabled(false); |
| } |
| |
| /** |
| * Configures search UI. |
| */ |
| private void setupSearchView() { |
| mSearchEditText = (SearchEditText)findViewById(R.id.search_src_text); |
| mSearchEditText.addTextChangedListener(this); |
| mSearchEditText.setOnEditorActionListener(this); |
| mSearchEditText.setText(mInitialFilter); |
| } |
| |
| private String getContactDisplayName(long contactId) { |
| String contactName = null; |
| Cursor c = getContentResolver().query( |
| ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), |
| new String[] {Contacts.DISPLAY_NAME}, null, null, null); |
| try { |
| if (c != null && c.moveToFirst()) { |
| contactName = c.getString(0); |
| } |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| |
| if (contactName == null) { |
| contactName = ""; |
| } |
| |
| return contactName; |
| } |
| |
| private int getSummaryDisplayNameColumnIndex() { |
| if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { |
| return SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX; |
| } else { |
| return SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX; |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void onClick(View v) { |
| int id = v.getId(); |
| switch (id) { |
| // TODO a better way of identifying the button |
| case android.R.id.button1: { |
| final int position = (Integer)v.getTag(); |
| Cursor c = mAdapter.getCursor(); |
| if (c != null) { |
| c.moveToPosition(position); |
| callContact(c); |
| } |
| break; |
| } |
| } |
| } |
| |
| private void setEmptyText() { |
| if (mMode == MODE_JOIN_CONTACT || mSearchMode) { |
| return; |
| } |
| |
| TextView empty = (TextView) findViewById(R.id.emptyText); |
| if (mDisplayOnlyPhones) { |
| empty.setText(getText(R.string.noContactsWithPhoneNumbers)); |
| } else if (mMode == MODE_STREQUENT || mMode == MODE_STARRED) { |
| empty.setText(getText(R.string.noFavoritesHelpText)); |
| } else if (mMode == MODE_QUERY || mMode == MODE_QUERY_PICK |
| || mMode == MODE_QUERY_PICK_PHONE || mMode == MODE_QUERY_PICK_TO_VIEW |
| || mMode == MODE_QUERY_PICK_TO_EDIT) { |
| empty.setText(getText(R.string.noMatchingContacts)); |
| } else { |
| boolean hasSim = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)) |
| .hasIccCard(); |
| boolean createShortcut = Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction()); |
| if (isSyncActive()) { |
| if (createShortcut) { |
| // Help text is the same no matter whether there is SIM or not. |
| empty.setText(getText(R.string.noContactsHelpTextWithSyncForCreateShortcut)); |
| } else if (hasSim) { |
| empty.setText(getText(R.string.noContactsHelpTextWithSync)); |
| } else { |
| empty.setText(getText(R.string.noContactsNoSimHelpTextWithSync)); |
| } |
| } else { |
| if (createShortcut) { |
| // Help text is the same no matter whether there is SIM or not. |
| empty.setText(getText(R.string.noContactsHelpTextForCreateShortcut)); |
| } else if (hasSim) { |
| empty.setText(getText(R.string.noContactsHelpText)); |
| } else { |
| empty.setText(getText(R.string.noContactsNoSimHelpText)); |
| } |
| } |
| } |
| } |
| |
| private boolean isSyncActive() { |
| Account[] accounts = AccountManager.get(this).getAccounts(); |
| if (accounts != null && accounts.length > 0) { |
| IContentService contentService = ContentResolver.getContentService(); |
| for (Account account : accounts) { |
| try { |
| if (contentService.isSyncActive(account, ContactsContract.AUTHORITY)) { |
| return true; |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not get the sync status"); |
| } |
| } |
| } |
| return false; |
| } |
| |
| private void buildUserGroupUri(String group) { |
| mGroupUri = Uri.withAppendedPath(Contacts.CONTENT_GROUP_URI, group); |
| } |
| |
| /** |
| * Sets the mode when the request is for "default" |
| */ |
| private void setDefaultMode() { |
| // Load the preferences |
| SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); |
| |
| mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES, |
| Prefs.DISPLAY_ONLY_PHONES_DEFAULT); |
| } |
| |
| @Override |
| protected void onDestroy() { |
| super.onDestroy(); |
| mPhotoLoader.stop(); |
| } |
| |
| @Override |
| protected void onStart() { |
| super.onStart(); |
| |
| mContactsPrefs.registerChangeListener(mPreferencesChangeListener); |
| } |
| |
| @Override |
| protected void onPause() { |
| super.onPause(); |
| unregisterProviderStatusObserver(); |
| } |
| |
| @Override |
| protected void onResume() { |
| super.onResume(); |
| |
| registerProviderStatusObserver(); |
| mPhotoLoader.resume(); |
| |
| Activity parent = getParent(); |
| |
| // Do this before setting the filter. The filter thread relies |
| // on some state that is initialized in setDefaultMode |
| if (mMode == MODE_DEFAULT) { |
| // If we're in default mode we need to possibly reset the mode due to a change |
| // in the preferences activity while we weren't running |
| setDefaultMode(); |
| } |
| |
| // See if we were invoked with a filter |
| if (mSearchMode) { |
| mSearchEditText.requestFocus(); |
| } |
| |
| if (!mSearchMode && !checkProviderState(mJustCreated)) { |
| return; |
| } |
| |
| if (mJustCreated) { |
| // We need to start a query here the first time the activity is launched, as long |
| // as we aren't doing a filter. |
| startQuery(); |
| } |
| mJustCreated = false; |
| mSearchInitiated = false; |
| } |
| |
| /** |
| * Obtains the contacts provider status and configures the UI accordingly. |
| * |
| * @param loadData true if the method needs to start a query when the |
| * provider is in the normal state |
| * @return true if the provider status is normal |
| */ |
| private boolean checkProviderState(boolean loadData) { |
| View importFailureView = findViewById(R.id.import_failure); |
| if (importFailureView == null) { |
| return true; |
| } |
| |
| TextView messageView = (TextView) findViewById(R.id.emptyText); |
| |
| // This query can be performed on the UI thread because |
| // the API explicitly allows such use. |
| Cursor cursor = getContentResolver().query(ProviderStatus.CONTENT_URI, new String[] { |
| ProviderStatus.STATUS, ProviderStatus.DATA1 |
| }, null, null, null); |
| try { |
| if (cursor.moveToFirst()) { |
| int status = cursor.getInt(0); |
| if (status != mProviderStatus) { |
| mProviderStatus = status; |
| switch (status) { |
| case ProviderStatus.STATUS_NORMAL: |
| mAdapter.notifyDataSetInvalidated(); |
| if (loadData) { |
| startQuery(); |
| } |
| break; |
| |
| case ProviderStatus.STATUS_CHANGING_LOCALE: |
| messageView.setText(R.string.locale_change_in_progress); |
| mAdapter.changeCursor(null); |
| mAdapter.notifyDataSetInvalidated(); |
| break; |
| |
| case ProviderStatus.STATUS_UPGRADING: |
| messageView.setText(R.string.upgrade_in_progress); |
| mAdapter.changeCursor(null); |
| mAdapter.notifyDataSetInvalidated(); |
| break; |
| |
| case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY: |
| long size = cursor.getLong(1); |
| String message = getResources().getString( |
| R.string.upgrade_out_of_memory, new Object[] {size}); |
| messageView.setText(message); |
| configureImportFailureView(importFailureView); |
| mAdapter.changeCursor(null); |
| mAdapter.notifyDataSetInvalidated(); |
| break; |
| } |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| |
| importFailureView.setVisibility( |
| mProviderStatus == ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY |
| ? View.VISIBLE |
| : View.GONE); |
| return mProviderStatus == ProviderStatus.STATUS_NORMAL; |
| } |
| |
| private void configureImportFailureView(View importFailureView) { |
| |
| OnClickListener listener = new OnClickListener(){ |
| |
| public void onClick(View v) { |
| switch(v.getId()) { |
| case R.id.import_failure_uninstall_apps: { |
| startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS)); |
| break; |
| } |
| case R.id.import_failure_retry_upgrade: { |
| // Send a provider status update, which will trigger a retry |
| ContentValues values = new ContentValues(); |
| values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING); |
| getContentResolver().update(ProviderStatus.CONTENT_URI, values, null, null); |
| break; |
| } |
| } |
| }}; |
| |
| Button uninstallApps = (Button) findViewById(R.id.import_failure_uninstall_apps); |
| uninstallApps.setOnClickListener(listener); |
| |
| Button retryUpgrade = (Button) findViewById(R.id.import_failure_retry_upgrade); |
| retryUpgrade.setOnClickListener(listener); |
| } |
| |
| private String getTextFilter() { |
| if (mSearchEditText != null) { |
| return mSearchEditText.getText().toString(); |
| } |
| return null; |
| } |
| |
| @Override |
| protected void onRestart() { |
| super.onRestart(); |
| |
| if (!checkProviderState(false)) { |
| return; |
| } |
| |
| // The cursor was killed off in onStop(), so we need to get a new one here |
| // We do not perform the query if a filter is set on the list because the |
| // filter will cause the query to happen anyway |
| if (TextUtils.isEmpty(getTextFilter())) { |
| startQuery(); |
| } else { |
| // Run the filtered query on the adapter |
| mAdapter.onContentChanged(); |
| } |
| } |
| |
| @Override |
| protected void onSaveInstanceState(Bundle icicle) { |
| super.onSaveInstanceState(icicle); |
| // Save list state in the bundle so we can restore it after the QueryHandler has run |
| if (mList != null) { |
| icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState()); |
| } |
| } |
| |
| @Override |
| protected void onRestoreInstanceState(Bundle icicle) { |
| super.onRestoreInstanceState(icicle); |
| // Retrieve list state. This will be applied after the QueryHandler has run |
| mListState = icicle.getParcelable(LIST_STATE_KEY); |
| } |
| |
| @Override |
| protected void onStop() { |
| super.onStop(); |
| |
| mContactsPrefs.unregisterChangeListener(); |
| mAdapter.setSuggestionsCursor(null); |
| mAdapter.changeCursor(null); |
| |
| if (mMode == MODE_QUERY) { |
| // Make sure the search box is closed |
| SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); |
| searchManager.stopSearch(); |
| } |
| } |
| |
| @Override |
| public boolean onCreateOptionsMenu(Menu menu) { |
| super.onCreateOptionsMenu(menu); |
| |
| // If Contacts was invoked by another Activity simply as a way of |
| // picking a contact, don't show the options menu |
| if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) { |
| return false; |
| } |
| |
| MenuInflater inflater = getMenuInflater(); |
| inflater.inflate(R.menu.list, menu); |
| return true; |
| } |
| |
| @Override |
| public boolean onPrepareOptionsMenu(Menu menu) { |
| final boolean defaultMode = (mMode == MODE_DEFAULT); |
| menu.findItem(R.id.menu_display_groups).setVisible(defaultMode); |
| return true; |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| switch (item.getItemId()) { |
| case R.id.menu_display_groups: { |
| final Intent intent = new Intent(this, ContactsPreferencesActivity.class); |
| startActivityForResult(intent, SUBACTIVITY_DISPLAY_GROUP); |
| return true; |
| } |
| case R.id.menu_search: { |
| onSearchRequested(); |
| return true; |
| } |
| case R.id.menu_add: { |
| final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); |
| startActivity(intent); |
| return true; |
| } |
| case R.id.menu_import_export: { |
| displayImportExportDialog(); |
| return true; |
| } |
| case R.id.menu_accounts: { |
| final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS); |
| intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { |
| ContactsContract.AUTHORITY |
| }); |
| startActivity(intent); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, |
| boolean globalSearch) { |
| if (mProviderStatus != ProviderStatus.STATUS_NORMAL) { |
| return; |
| } |
| |
| if (globalSearch) { |
| super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); |
| } else { |
| if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) { |
| if ((mMode & MODE_MASK_PICKER) != 0) { |
| ContactsSearchManager.startSearchForResult(this, initialQuery, |
| SUBACTIVITY_FILTER); |
| } else { |
| ContactsSearchManager.startSearch(this, initialQuery); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Performs filtering of the list based on the search query entered in the |
| * search text edit. |
| */ |
| protected void onSearchTextChanged() { |
| // Set the proper empty string |
| setEmptyText(); |
| |
| Filter filter = mAdapter.getFilter(); |
| filter.filter(getTextFilter()); |
| } |
| |
| /** |
| * Starts a new activity that will run a search query and display search results. |
| */ |
| private void doSearch() { |
| String query = getTextFilter(); |
| if (TextUtils.isEmpty(query)) { |
| return; |
| } |
| |
| Intent intent = new Intent(this, SearchResultsActivity.class); |
| Intent originalIntent = getIntent(); |
| Bundle originalExtras = originalIntent.getExtras(); |
| if (originalExtras != null) { |
| intent.putExtras(originalExtras); |
| } |
| |
| intent.putExtra(SearchManager.QUERY, query); |
| if ((mMode & MODE_MASK_PICKER) != 0) { |
| intent.setAction(ACTION_SEARCH_INTERNAL); |
| intent.putExtra(SHORTCUT_ACTION_KEY, mShortcutAction); |
| if (mShortcutAction != null) { |
| if (Intent.ACTION_CALL.equals(mShortcutAction) |
| || Intent.ACTION_SENDTO.equals(mShortcutAction)) { |
| intent.putExtra(Insert.PHONE, query); |
| } |
| } else { |
| switch (mQueryMode) { |
| case QUERY_MODE_MAILTO: |
| intent.putExtra(Insert.EMAIL, query); |
| break; |
| case QUERY_MODE_TEL: |
| intent.putExtra(Insert.PHONE, query); |
| break; |
| } |
| } |
| startActivityForResult(intent, SUBACTIVITY_SEARCH); |
| } else { |
| intent.setAction(Intent.ACTION_SEARCH); |
| startActivity(intent); |
| } |
| } |
| |
| @Override |
| protected Dialog onCreateDialog(int id, Bundle bundle) { |
| switch (id) { |
| case R.string.import_from_sim: |
| case R.string.import_from_sdcard: { |
| return AccountSelectionUtil.getSelectAccountDialog(this, id); |
| } |
| case R.id.dialog_sdcard_not_found: { |
| return new AlertDialog.Builder(this) |
| .setTitle(R.string.no_sdcard_title) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setMessage(R.string.no_sdcard_message) |
| .setPositiveButton(android.R.string.ok, null).create(); |
| } |
| case R.id.dialog_delete_contact_confirmation: { |
| return new AlertDialog.Builder(this) |
| .setTitle(R.string.deleteConfirmation_title) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setMessage(R.string.deleteConfirmation) |
| .setNegativeButton(android.R.string.cancel, null) |
| .setPositiveButton(android.R.string.ok, |
| new DeleteClickListener()).create(); |
| } |
| case R.id.dialog_readonly_contact_hide_confirmation: { |
| return new AlertDialog.Builder(this) |
| .setTitle(R.string.deleteConfirmation_title) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setMessage(R.string.readOnlyContactWarning) |
| .setNegativeButton(android.R.string.cancel, null) |
| .setPositiveButton(android.R.string.ok, |
| new DeleteClickListener()).create(); |
| } |
| case R.id.dialog_readonly_contact_delete_confirmation: { |
| return new AlertDialog.Builder(this) |
| .setTitle(R.string.deleteConfirmation_title) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setMessage(R.string.readOnlyContactDeleteConfirmation) |
| .setNegativeButton(android.R.string.cancel, null) |
| .setPositiveButton(android.R.string.ok, |
| new DeleteClickListener()).create(); |
| } |
| case R.id.dialog_multiple_contact_delete_confirmation: { |
| return new AlertDialog.Builder(this) |
| .setTitle(R.string.deleteConfirmation_title) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setMessage(R.string.multipleContactDeleteConfirmation) |
| .setNegativeButton(android.R.string.cancel, null) |
| .setPositiveButton(android.R.string.ok, |
| new DeleteClickListener()).create(); |
| } |
| } |
| return super.onCreateDialog(id, bundle); |
| } |
| |
| /** |
| * Create a {@link Dialog} that allows the user to pick from a bulk import |
| * or bulk export task across all contacts. |
| */ |
| private void displayImportExportDialog() { |
| // Wrap our context to inflate list items using correct theme |
| final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light); |
| final Resources res = dialogContext.getResources(); |
| final LayoutInflater dialogInflater = (LayoutInflater)dialogContext |
| .getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| |
| // Adapter that shows a list of string resources |
| final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(this, |
| android.R.layout.simple_list_item_1) { |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| if (convertView == null) { |
| convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1, |
| parent, false); |
| } |
| |
| final int resId = this.getItem(position); |
| ((TextView)convertView).setText(resId); |
| return convertView; |
| } |
| }; |
| |
| if (TelephonyManager.getDefault().hasIccCard()) { |
| adapter.add(R.string.import_from_sim); |
| } |
| if (res.getBoolean(R.bool.config_allow_import_from_sdcard)) { |
| adapter.add(R.string.import_from_sdcard); |
| } |
| if (res.getBoolean(R.bool.config_allow_export_to_sdcard)) { |
| adapter.add(R.string.export_to_sdcard); |
| } |
| if (res.getBoolean(R.bool.config_allow_share_visible_contacts)) { |
| adapter.add(R.string.share_visible_contacts); |
| } |
| |
| final DialogInterface.OnClickListener clickListener = |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| dialog.dismiss(); |
| |
| final int resId = adapter.getItem(which); |
| switch (resId) { |
| case R.string.import_from_sim: |
| case R.string.import_from_sdcard: { |
| handleImportRequest(resId); |
| break; |
| } |
| case R.string.export_to_sdcard: { |
| Context context = ContactsListActivity.this; |
| Intent exportIntent = new Intent(context, ExportVCardActivity.class); |
| context.startActivity(exportIntent); |
| break; |
| } |
| case R.string.share_visible_contacts: { |
| doShareVisibleContacts(); |
| break; |
| } |
| default: { |
| Log.e(TAG, "Unexpected resource: " + |
| getResources().getResourceEntryName(resId)); |
| } |
| } |
| } |
| }; |
| |
| new AlertDialog.Builder(this) |
| .setTitle(R.string.dialog_import_export) |
| .setNegativeButton(android.R.string.cancel, null) |
| .setSingleChoiceItems(adapter, -1, clickListener) |
| .show(); |
| } |
| |
| private void doShareVisibleContacts() { |
| final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI, |
| sLookupProjection, getContactSelection(), null, null); |
| try { |
| if (!cursor.moveToFirst()) { |
| Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); |
| return; |
| } |
| |
| StringBuilder uriListBuilder = new StringBuilder(); |
| int index = 0; |
| for (;!cursor.isAfterLast(); cursor.moveToNext()) { |
| if (index != 0) |
| uriListBuilder.append(':'); |
| uriListBuilder.append(cursor.getString(0)); |
| index++; |
| } |
| Uri uri = Uri.withAppendedPath( |
| Contacts.CONTENT_MULTI_VCARD_URI, |
| Uri.encode(uriListBuilder.toString())); |
| |
| final Intent intent = new Intent(Intent.ACTION_SEND); |
| intent.setType(Contacts.CONTENT_VCARD_TYPE); |
| intent.putExtra(Intent.EXTRA_STREAM, uri); |
| startActivity(intent); |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| private void handleImportRequest(int resId) { |
| // There's three possibilities: |
| // - more than one accounts -> ask the user |
| // - just one account -> use the account without asking the user |
| // - no account -> use phone-local storage without asking the user |
| final Sources sources = Sources.getInstance(this); |
| final List<Account> accountList = sources.getAccounts(true); |
| final int size = accountList.size(); |
| if (size > 1) { |
| showDialog(resId); |
| return; |
| } |
| |
| AccountSelectionUtil.doImport(this, resId, (size == 1 ? accountList.get(0) : null)); |
| } |
| |
| @Override |
| protected void onActivityResult(int requestCode, int resultCode, Intent data) { |
| switch (requestCode) { |
| case SUBACTIVITY_NEW_CONTACT: |
| if (resultCode == RESULT_OK) { |
| returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME), |
| data.getData(), (mMode & MODE_MASK_PICKER) != 0 |
| ? Intent.FLAG_GRANT_READ_URI_PERMISSION : 0); |
| } |
| break; |
| |
| case SUBACTIVITY_VIEW_CONTACT: |
| if (resultCode == RESULT_OK) { |
| mAdapter.notifyDataSetChanged(); |
| } |
| break; |
| |
| case SUBACTIVITY_DISPLAY_GROUP: |
| // Mark as just created so we re-run the view query |
| mJustCreated = true; |
| break; |
| |
| case SUBACTIVITY_FILTER: |
| case SUBACTIVITY_SEARCH: |
| // Pass through results of filter or search UI |
| if (resultCode == RESULT_OK) { |
| setResult(RESULT_OK, data); |
| finish(); |
| } |
| break; |
| } |
| } |
| |
| @Override |
| public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { |
| // If Contacts was invoked by another Activity simply as a way of |
| // picking a contact, don't show the context menu |
| if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) { |
| return; |
| } |
| |
| AdapterView.AdapterContextMenuInfo info; |
| try { |
| info = (AdapterView.AdapterContextMenuInfo) menuInfo; |
| } catch (ClassCastException e) { |
| Log.e(TAG, "bad menuInfo", e); |
| return; |
| } |
| |
| Cursor cursor = (Cursor) getListAdapter().getItem(info.position); |
| if (cursor == null) { |
| // For some reason the requested item isn't available, do nothing |
| return; |
| } |
| long id = info.id; |
| Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id); |
| long rawContactId = ContactsUtils.queryForRawContactId(getContentResolver(), id); |
| Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); |
| |
| // Setup the menu header |
| menu.setHeaderTitle(cursor.getString(getSummaryDisplayNameColumnIndex())); |
| |
| // View contact details |
| final Intent viewContactIntent = new Intent(Intent.ACTION_VIEW, contactUri); |
| StickyTabs.setTab(viewContactIntent, getIntent()); |
| menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact) |
| .setIntent(viewContactIntent); |
| |
| if (cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) { |
| // Calling contact |
| menu.add(0, MENU_ITEM_CALL, 0, getString(R.string.menu_call)); |
| // Send SMS item |
| menu.add(0, MENU_ITEM_SEND_SMS, 0, getString(R.string.menu_sendSMS)); |
| } |
| |
| // Star toggling |
| int starState = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX); |
| if (starState == 0) { |
| menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar); |
| } else { |
| menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar); |
| } |
| |
| // Contact editing |
| menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact) |
| .setIntent(new Intent(Intent.ACTION_EDIT, rawContactUri)); |
| menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact); |
| } |
| |
| @Override |
| public boolean onContextItemSelected(MenuItem item) { |
| AdapterView.AdapterContextMenuInfo info; |
| try { |
| info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); |
| } catch (ClassCastException e) { |
| Log.e(TAG, "bad menuInfo", e); |
| return false; |
| } |
| |
| Cursor cursor = (Cursor) getListAdapter().getItem(info.position); |
| |
| switch (item.getItemId()) { |
| case MENU_ITEM_TOGGLE_STAR: { |
| // Toggle the star |
| ContentValues values = new ContentValues(1); |
| values.put(Contacts.STARRED, cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX) == 0 ? 1 : 0); |
| final Uri selectedUri = this.getContactUri(info.position); |
| getContentResolver().update(selectedUri, values, null, null); |
| return true; |
| } |
| |
| case MENU_ITEM_CALL: { |
| callContact(cursor); |
| return true; |
| } |
| |
| case MENU_ITEM_SEND_SMS: { |
| smsContact(cursor); |
| return true; |
| } |
| |
| case MENU_ITEM_DELETE: { |
| doContactDelete(getContactUri(info.position)); |
| return true; |
| } |
| } |
| |
| return super.onContextItemSelected(item); |
| } |
| |
| /** |
| * Event handler for the use case where the user starts typing without |
| * bringing up the search UI first. |
| */ |
| public boolean onKey(View v, int keyCode, KeyEvent event) { |
| if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0 && !mSearchInitiated) { |
| int unicodeChar = event.getUnicodeChar(); |
| if (unicodeChar != 0) { |
| mSearchInitiated = true; |
| startSearch(new String(new int[]{unicodeChar}, 0, 1), false, null, false); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Event handler for search UI. |
| */ |
| public void afterTextChanged(Editable s) { |
| onSearchTextChanged(); |
| } |
| |
| public void beforeTextChanged(CharSequence s, int start, int count, int after) { |
| } |
| |
| public void onTextChanged(CharSequence s, int start, int before, int count) { |
| } |
| |
| /** |
| * Event handler for search UI. |
| */ |
| public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { |
| if (actionId == EditorInfo.IME_ACTION_DONE) { |
| hideSoftKeyboard(); |
| if (TextUtils.isEmpty(getTextFilter())) { |
| finish(); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_CALL: { |
| if (callSelection()) { |
| return true; |
| } |
| break; |
| } |
| |
| case KeyEvent.KEYCODE_DEL: { |
| if (deleteSelection()) { |
| return true; |
| } |
| break; |
| } |
| } |
| |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| private boolean deleteSelection() { |
| if ((mMode & MODE_MASK_PICKER) != 0) { |
| return false; |
| } |
| |
| final int position = getListView().getSelectedItemPosition(); |
| if (position != ListView.INVALID_POSITION) { |
| Uri contactUri = getContactUri(position); |
| if (contactUri != null) { |
| doContactDelete(contactUri); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Prompt the user before deleting the given {@link Contacts} entry. |
| */ |
| protected void doContactDelete(Uri contactUri) { |
| mReadOnlySourcesCnt = 0; |
| mWritableSourcesCnt = 0; |
| mWritableRawContactIds.clear(); |
| |
| Sources sources = Sources.getInstance(ContactsListActivity.this); |
| Cursor c = getContentResolver().query(RawContacts.CONTENT_URI, RAW_CONTACTS_PROJECTION, |
| RawContacts.CONTACT_ID + "=" + ContentUris.parseId(contactUri), null, |
| null); |
| if (c != null) { |
| try { |
| while (c.moveToNext()) { |
| final String accountType = c.getString(2); |
| final long rawContactId = c.getLong(0); |
| ContactsSource contactsSource = sources.getInflatedSource(accountType, |
| ContactsSource.LEVEL_SUMMARY); |
| if (contactsSource != null && contactsSource.readOnly) { |
| mReadOnlySourcesCnt += 1; |
| } else { |
| mWritableSourcesCnt += 1; |
| mWritableRawContactIds.add(rawContactId); |
| } |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| |
| mSelectedContactUri = contactUri; |
| if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt > 0) { |
| showDialog(R.id.dialog_readonly_contact_delete_confirmation); |
| } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) { |
| showDialog(R.id.dialog_readonly_contact_hide_confirmation); |
| } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) { |
| showDialog(R.id.dialog_multiple_contact_delete_confirmation); |
| } else { |
| showDialog(R.id.dialog_delete_contact_confirmation); |
| } |
| } |
| |
| /** |
| * Dismisses the soft keyboard when the list takes focus. |
| */ |
| public void onFocusChange(View view, boolean hasFocus) { |
| if (view == getListView() && hasFocus) { |
| hideSoftKeyboard(); |
| } |
| } |
| |
| /** |
| * Dismisses the soft keyboard when the list takes focus. |
| */ |
| public boolean onTouch(View view, MotionEvent event) { |
| if (view == getListView()) { |
| hideSoftKeyboard(); |
| } |
| return false; |
| } |
| |
| /** |
| * Dismisses the search UI along with the keyboard if the filter text is empty. |
| */ |
| public boolean onKeyPreIme(int keyCode, KeyEvent event) { |
| if (mSearchMode && keyCode == KeyEvent.KEYCODE_BACK && TextUtils.isEmpty(getTextFilter())) { |
| hideSoftKeyboard(); |
| onBackPressed(); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| protected void onListItemClick(ListView l, View v, int position, long id) { |
| hideSoftKeyboard(); |
| |
| if (mSearchMode && mAdapter.isSearchAllContactsItemPosition(position)) { |
| doSearch(); |
| } else if (mMode == MODE_INSERT_OR_EDIT_CONTACT || mMode == MODE_QUERY_PICK_TO_EDIT) { |
| Intent intent; |
| if (position == 0 && !mSearchMode && mMode != MODE_QUERY_PICK_TO_EDIT) { |
| intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); |
| } else { |
| intent = new Intent(Intent.ACTION_EDIT, getSelectedUri(position)); |
| } |
| intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); |
| Bundle extras = getIntent().getExtras(); |
| if (extras != null) { |
| intent.putExtras(extras); |
| } |
| intent.putExtra(KEY_PICKER_MODE, (mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER); |
| |
| startActivity(intent); |
| finish(); |
| } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW |
| && position == 0) { |
| Intent newContact = new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI); |
| startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT); |
| } else if (mMode == MODE_JOIN_CONTACT && id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) { |
| mJoinModeShowAllContacts = false; |
| startQuery(); |
| } else if (id > 0) { |
| final Uri uri = getSelectedUri(position); |
| if ((mMode & MODE_MASK_PICKER) == 0) { |
| final Intent intent = new Intent(Intent.ACTION_VIEW, uri); |
| StickyTabs.setTab(intent, getIntent()); |
| startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT); |
| } else if (mMode == MODE_JOIN_CONTACT) { |
| returnPickerResult(null, null, uri, 0); |
| } else if (mMode == MODE_QUERY_PICK_TO_VIEW) { |
| // Started with query that should launch to view contact |
| final Intent intent = new Intent(Intent.ACTION_VIEW, uri); |
| startActivity(intent); |
| finish(); |
| } else if (mMode == MODE_PICK_PHONE || mMode == MODE_QUERY_PICK_PHONE) { |
| Cursor c = (Cursor) mAdapter.getItem(position); |
| returnPickerResult(c, c.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX), uri, |
| Intent.FLAG_GRANT_READ_URI_PERMISSION); |
| } else if ((mMode & MODE_MASK_PICKER) != 0) { |
| Cursor c = (Cursor) mAdapter.getItem(position); |
| returnPickerResult(c, c.getString(getSummaryDisplayNameColumnIndex()), uri, |
| Intent.FLAG_GRANT_READ_URI_PERMISSION); |
| } else if (mMode == MODE_PICK_POSTAL |
| || mMode == MODE_LEGACY_PICK_POSTAL |
| || mMode == MODE_LEGACY_PICK_PHONE) { |
| returnPickerResult(null, null, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); |
| } |
| } else { |
| signalError(); |
| } |
| } |
| |
| private void hideSoftKeyboard() { |
| // Hide soft keyboard, if visible |
| InputMethodManager inputMethodManager = (InputMethodManager) |
| getSystemService(Context.INPUT_METHOD_SERVICE); |
| inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0); |
| } |
| |
| /** |
| * @param selectedUri In most cases, this should be a lookup {@link Uri}, possibly |
| * generated through {@link Contacts#getLookupUri(long, String)}. |
| */ |
| private void returnPickerResult(Cursor c, String name, Uri selectedUri, int uriPerms) { |
| final Intent intent = new Intent(); |
| |
| if (mShortcutAction != null) { |
| Intent shortcutIntent; |
| if (Intent.ACTION_VIEW.equals(mShortcutAction)) { |
| // This is a simple shortcut to view a contact. |
| shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT); |
| shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | |
| Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); |
| |
| shortcutIntent.setData(selectedUri); |
| shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_MODE, |
| ContactsContract.QuickContact.MODE_LARGE); |
| shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES, |
| (String[]) null); |
| |
| final Bitmap icon = framePhoto(loadContactPhoto(selectedUri, null)); |
| if (icon != null) { |
| intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, scaleToAppIconSize(icon)); |
| } else { |
| intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, |
| Intent.ShortcutIconResource.fromContext(this, |
| R.drawable.ic_launcher_shortcut_contact)); |
| } |
| } else { |
| // This is a direct dial or sms shortcut. |
| String number = c.getString(PHONE_NUMBER_COLUMN_INDEX); |
| int type = c.getInt(PHONE_TYPE_COLUMN_INDEX); |
| String scheme; |
| int resid; |
| if (Intent.ACTION_CALL.equals(mShortcutAction)) { |
| scheme = Constants.SCHEME_TEL; |
| resid = R.drawable.badge_action_call; |
| } else { |
| scheme = Constants.SCHEME_SMSTO; |
| resid = R.drawable.badge_action_sms; |
| } |
| |
| // Make the URI a direct tel: URI so that it will always continue to work |
| Uri phoneUri = Uri.fromParts(scheme, number, null); |
| shortcutIntent = new Intent(mShortcutAction, phoneUri); |
| |
| intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, |
| generatePhoneNumberIcon(selectedUri, type, resid)); |
| } |
| shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); |
| intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); |
| intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); |
| setResult(RESULT_OK, intent); |
| } else { |
| intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); |
| intent.addFlags(uriPerms); |
| setResult(RESULT_OK, intent.setData(selectedUri)); |
| } |
| finish(); |
| } |
| |
| private Bitmap framePhoto(Bitmap photo) { |
| final Resources r = getResources(); |
| final Drawable frame = r.getDrawable(com.android.internal.R.drawable.quickcontact_badge); |
| |
| final int width = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_width); |
| final int height = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_height); |
| |
| frame.setBounds(0, 0, width, height); |
| |
| final Rect padding = new Rect(); |
| frame.getPadding(padding); |
| |
| final Rect source = new Rect(0, 0, photo.getWidth(), photo.getHeight()); |
| final Rect destination = new Rect(padding.left, padding.top, |
| width - padding.right, height - padding.bottom); |
| |
| final int d = Math.max(width, height); |
| final Bitmap b = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888); |
| final Canvas c = new Canvas(b); |
| |
| c.translate((d - width) / 2.0f, (d - height) / 2.0f); |
| frame.draw(c); |
| c.drawBitmap(photo, source, destination, new Paint(Paint.FILTER_BITMAP_FLAG)); |
| |
| return b; |
| } |
| |
| /** |
| * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone |
| * number, and if there is a photo also adds the call action icon. |
| * |
| * @param lookupUri The person the phone number belongs to |
| * @param type The type of the phone number |
| * @param actionResId The ID for the action resource |
| * @return The bitmap for the icon |
| */ |
| private Bitmap generatePhoneNumberIcon(Uri lookupUri, int type, int actionResId) { |
| final Resources r = getResources(); |
| boolean drawPhoneOverlay = true; |
| final float scaleDensity = getResources().getDisplayMetrics().scaledDensity; |
| |
| Bitmap photo = loadContactPhoto(lookupUri, null); |
| if (photo == null) { |
| // If there isn't a photo use the generic phone action icon instead |
| Bitmap phoneIcon = getPhoneActionIcon(r, actionResId); |
| if (phoneIcon != null) { |
| photo = phoneIcon; |
| drawPhoneOverlay = false; |
| } else { |
| return null; |
| } |
| } |
| |
| // Setup the drawing classes |
| Bitmap icon = createShortcutBitmap(); |
| Canvas canvas = new Canvas(icon); |
| |
| // Copy in the photo |
| Paint photoPaint = new Paint(); |
| photoPaint.setDither(true); |
| photoPaint.setFilterBitmap(true); |
| Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight()); |
| Rect dst = new Rect(0,0, mIconSize, mIconSize); |
| canvas.drawBitmap(photo, src, dst, photoPaint); |
| |
| // Create an overlay for the phone number type |
| String overlay = null; |
| switch (type) { |
| case Phone.TYPE_HOME: |
| overlay = getString(R.string.type_short_home); |
| break; |
| |
| case Phone.TYPE_MOBILE: |
| overlay = getString(R.string.type_short_mobile); |
| break; |
| |
| case Phone.TYPE_WORK: |
| overlay = getString(R.string.type_short_work); |
| break; |
| |
| case Phone.TYPE_PAGER: |
| overlay = getString(R.string.type_short_pager); |
| break; |
| |
| case Phone.TYPE_OTHER: |
| overlay = getString(R.string.type_short_other); |
| break; |
| } |
| if (overlay != null) { |
| Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); |
| textPaint.setTextSize(20.0f * scaleDensity); |
| textPaint.setTypeface(Typeface.DEFAULT_BOLD); |
| textPaint.setColor(r.getColor(R.color.textColorIconOverlay)); |
| textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow)); |
| canvas.drawText(overlay, 2 * scaleDensity, 16 * scaleDensity, textPaint); |
| } |
| |
| // Draw the phone action icon as an overlay |
| if (ENABLE_ACTION_ICON_OVERLAYS && drawPhoneOverlay) { |
| Bitmap phoneIcon = getPhoneActionIcon(r, actionResId); |
| if (phoneIcon != null) { |
| src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight()); |
| int iconWidth = icon.getWidth(); |
| dst.set(iconWidth - ((int) (20 * scaleDensity)), -1, |
| iconWidth, ((int) (19 * scaleDensity))); |
| canvas.drawBitmap(phoneIcon, src, dst, photoPaint); |
| } |
| } |
| |
| return icon; |
| } |
| |
| private Bitmap scaleToAppIconSize(Bitmap photo) { |
| // Setup the drawing classes |
| Bitmap icon = createShortcutBitmap(); |
| Canvas canvas = new Canvas(icon); |
| |
| // Copy in the photo |
| Paint photoPaint = new Paint(); |
| photoPaint.setDither(true); |
| photoPaint.setFilterBitmap(true); |
| Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight()); |
| Rect dst = new Rect(0,0, mIconSize, mIconSize); |
| canvas.drawBitmap(photo, src, dst, photoPaint); |
| |
| return icon; |
| } |
| |
| private Bitmap createShortcutBitmap() { |
| return Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888); |
| } |
| |
| /** |
| * Returns the icon for the phone call action. |
| * |
| * @param r The resources to load the icon from |
| * @param resId The resource ID to load |
| * @return the icon for the phone call action |
| */ |
| private Bitmap getPhoneActionIcon(Resources r, int resId) { |
| Drawable phoneIcon = r.getDrawable(resId); |
| if (phoneIcon instanceof BitmapDrawable) { |
| BitmapDrawable bd = (BitmapDrawable) phoneIcon; |
| return bd.getBitmap(); |
| } else { |
| return null; |
| } |
| } |
| |
| private Uri getUriToQuery() { |
| switch(mMode) { |
| case MODE_JOIN_CONTACT: |
| return getJoinSuggestionsUri(null); |
| case MODE_FREQUENT: |
| case MODE_STARRED: |
| return Contacts.CONTENT_URI; |
| |
| case MODE_DEFAULT: |
| case MODE_CUSTOM: |
| case MODE_INSERT_OR_EDIT_CONTACT: |
| case MODE_PICK_CONTACT: |
| case MODE_PICK_OR_CREATE_CONTACT:{ |
| return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS; |
| } |
| case MODE_STREQUENT: { |
| return Contacts.CONTENT_STREQUENT_URI; |
| } |
| case MODE_LEGACY_PICK_PERSON: |
| case MODE_LEGACY_PICK_OR_CREATE_PERSON: { |
| return People.CONTENT_URI; |
| } |
| case MODE_PICK_PHONE: { |
| return buildSectionIndexerUri(Phone.CONTENT_URI); |
| } |
| case MODE_LEGACY_PICK_PHONE: { |
| return Phones.CONTENT_URI; |
| } |
| case MODE_PICK_POSTAL: { |
| return buildSectionIndexerUri(StructuredPostal.CONTENT_URI); |
| } |
| case MODE_LEGACY_PICK_POSTAL: { |
| return ContactMethods.CONTENT_URI; |
| } |
| case MODE_QUERY_PICK_TO_VIEW: { |
| if (mQueryMode == QUERY_MODE_MAILTO) { |
| return Uri.withAppendedPath(Email.CONTENT_FILTER_URI, |
| Uri.encode(mInitialFilter)); |
| } else if (mQueryMode == QUERY_MODE_TEL) { |
| return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, |
| Uri.encode(mInitialFilter)); |
| } |
| return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS; |
| } |
| case MODE_QUERY: |
| case MODE_QUERY_PICK: |
| case MODE_QUERY_PICK_TO_EDIT: { |
| return getContactFilterUri(mInitialFilter); |
| } |
| case MODE_QUERY_PICK_PHONE: { |
| return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, |
| Uri.encode(mInitialFilter)); |
| } |
| case MODE_GROUP: { |
| return mGroupUri; |
| } |
| default: { |
| throw new IllegalStateException("Can't generate URI: Unsupported Mode."); |
| } |
| } |
| } |
| |
| /** |
| * Build the {@link Contacts#CONTENT_LOOKUP_URI} for the given |
| * {@link ListView} position, using {@link #mAdapter}. |
| */ |
| private Uri getContactUri(int position) { |
| if (position == ListView.INVALID_POSITION) { |
| throw new IllegalArgumentException("Position not in list bounds"); |
| } |
| |
| final Cursor cursor = (Cursor)mAdapter.getItem(position); |
| if (cursor == null) { |
| return null; |
| } |
| |
| switch(mMode) { |
| case MODE_LEGACY_PICK_PERSON: |
| case MODE_LEGACY_PICK_OR_CREATE_PERSON: { |
| final long personId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX); |
| return ContentUris.withAppendedId(People.CONTENT_URI, personId); |
| } |
| |
| default: { |
| // Build and return soft, lookup reference |
| final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX); |
| final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX); |
| return Contacts.getLookupUri(contactId, lookupKey); |
| } |
| } |
| } |
| |
| /** |
| * Build the {@link Uri} for the given {@link ListView} position, which can |
| * be used as result when in {@link #MODE_MASK_PICKER} mode. |
| */ |
| private Uri getSelectedUri(int position) { |
| if (position == ListView.INVALID_POSITION) { |
| throw new IllegalArgumentException("Position not in list bounds"); |
| } |
| |
| final long id = mAdapter.getItemId(position); |
| switch(mMode) { |
| case MODE_LEGACY_PICK_PERSON: |
| case MODE_LEGACY_PICK_OR_CREATE_PERSON: { |
| return ContentUris.withAppendedId(People.CONTENT_URI, id); |
| } |
| case MODE_PICK_PHONE: |
| case MODE_QUERY_PICK_PHONE: { |
| return ContentUris.withAppendedId(Data.CONTENT_URI, id); |
| } |
| case MODE_LEGACY_PICK_PHONE: { |
| return ContentUris.withAppendedId(Phones.CONTENT_URI, id); |
| } |
| case MODE_PICK_POSTAL: { |
| return ContentUris.withAppendedId(Data.CONTENT_URI, id); |
| } |
| case MODE_LEGACY_PICK_POSTAL: { |
| return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id); |
| } |
| default: { |
| return getContactUri(position); |
| } |
| } |
| } |
| |
| String[] getProjectionForQuery() { |
| switch(mMode) { |
| case MODE_JOIN_CONTACT: |
| case MODE_STREQUENT: |
| case MODE_FREQUENT: |
| case MODE_STARRED: |
| case MODE_DEFAULT: |
| case MODE_CUSTOM: |
| case MODE_INSERT_OR_EDIT_CONTACT: |
| case MODE_GROUP: |
| case MODE_PICK_CONTACT: |
| case MODE_PICK_OR_CREATE_CONTACT: { |
| return mSearchMode |
| ? CONTACTS_SUMMARY_FILTER_PROJECTION |
| : CONTACTS_SUMMARY_PROJECTION; |
| } |
| case MODE_QUERY: |
| case MODE_QUERY_PICK: |
| case MODE_QUERY_PICK_TO_EDIT: { |
| return CONTACTS_SUMMARY_FILTER_PROJECTION; |
| } |
| case MODE_LEGACY_PICK_PERSON: |
| case MODE_LEGACY_PICK_OR_CREATE_PERSON: { |
| return LEGACY_PEOPLE_PROJECTION ; |
| } |
| case MODE_QUERY_PICK_PHONE: |
| case MODE_PICK_PHONE: { |
| return PHONES_PROJECTION; |
| } |
| case MODE_LEGACY_PICK_PHONE: { |
| return LEGACY_PHONES_PROJECTION; |
| } |
| case MODE_PICK_POSTAL: { |
| return POSTALS_PROJECTION; |
| } |
| case MODE_LEGACY_PICK_POSTAL: { |
| return LEGACY_POSTALS_PROJECTION; |
| } |
| case MODE_QUERY_PICK_TO_VIEW: { |
| if (mQueryMode == QUERY_MODE_MAILTO) { |
| return CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL; |
| } else if (mQueryMode == QUERY_MODE_TEL) { |
| return PHONES_PROJECTION; |
| } |
| break; |
| } |
| } |
| |
| // Default to normal aggregate projection |
| return CONTACTS_SUMMARY_PROJECTION; |
| } |
| |
| private Bitmap loadContactPhoto(Uri selectedUri, BitmapFactory.Options options) { |
| Uri contactUri = null; |
| if (Contacts.CONTENT_ITEM_TYPE.equals(getContentResolver().getType(selectedUri))) { |
| // TODO we should have a "photo" directory under the lookup URI itself |
| contactUri = Contacts.lookupContact(getContentResolver(), selectedUri); |
| } else { |
| |
| Cursor cursor = getContentResolver().query(selectedUri, |
| new String[] { Data.CONTACT_ID }, null, null, null); |
| try { |
| if (cursor != null && cursor.moveToFirst()) { |
| final long contactId = cursor.getLong(0); |
| contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); |
| } |
| } finally { |
| if (cursor != null) cursor.close(); |
| } |
| } |
| |
| Cursor cursor = null; |
| Bitmap bm = null; |
| |
| try { |
| Uri photoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY); |
| cursor = getContentResolver().query(photoUri, new String[] {Photo.PHOTO}, |
| null, null, null); |
| if (cursor != null && cursor.moveToFirst()) { |
| bm = ContactsUtils.loadContactPhoto(cursor, 0, options); |
| } |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| if (bm == null) { |
| final int[] fallbacks = { |
| R.drawable.ic_contact_picture, |
| R.drawable.ic_contact_picture_2, |
| R.drawable.ic_contact_picture_3 |
| }; |
| bm = BitmapFactory.decodeResource(getResources(), |
| fallbacks[new Random().nextInt(fallbacks.length)]); |
| } |
| |
| return bm; |
| } |
| |
| /** |
| * Return the selection arguments for a default query based on the |
| * {@link #mDisplayOnlyPhones} flag. |
| */ |
| private String getContactSelection() { |
| if (mDisplayOnlyPhones) { |
| return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES; |
| } else { |
| return CLAUSE_ONLY_VISIBLE; |
| } |
| } |
| |
| private Uri getContactFilterUri(String filter) { |
| Uri baseUri; |
| if (!TextUtils.isEmpty(filter)) { |
| baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter)); |
| } else { |
| baseUri = Contacts.CONTENT_URI; |
| } |
| |
| if (mAdapter.getDisplaySectionHeadersEnabled()) { |
| return buildSectionIndexerUri(baseUri); |
| } else { |
| return baseUri; |
| } |
| } |
| |
| private Uri getPeopleFilterUri(String filter) { |
| if (!TextUtils.isEmpty(filter)) { |
| return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter)); |
| } else { |
| return People.CONTENT_URI; |
| } |
| } |
| |
| private static Uri buildSectionIndexerUri(Uri uri) { |
| return uri.buildUpon() |
| .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build(); |
| } |
| |
| private Uri getJoinSuggestionsUri(String filter) { |
| Builder builder = Contacts.CONTENT_URI.buildUpon(); |
| builder.appendEncodedPath(String.valueOf(mQueryAggregateId)); |
| builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY); |
| if (!TextUtils.isEmpty(filter)) { |
| builder.appendEncodedPath(Uri.encode(filter)); |
| } |
| builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS)); |
| return builder.build(); |
| } |
| |
| private String getSortOrder(String[] projectionType) { |
| if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY) { |
| return Contacts.SORT_KEY_PRIMARY; |
| } else { |
| return Contacts.SORT_KEY_ALTERNATIVE; |
| } |
| } |
| |
| void startQuery() { |
| // Set the proper empty string |
| setEmptyText(); |
| |
| if (mSearchResultsMode) { |
| TextView foundContactsText = (TextView)findViewById(R.id.search_results_found); |
| foundContactsText.setText(R.string.search_results_searching); |
| } |
| |
| mAdapter.setLoading(true); |
| |
| // Cancel any pending queries |
| mQueryHandler.cancelOperation(QUERY_TOKEN); |
| mQueryHandler.setLoadingJoinSuggestions(false); |
| |
| mSortOrder = mContactsPrefs.getSortOrder(); |
| mDisplayOrder = mContactsPrefs.getDisplayOrder(); |
| |
| // When sort order and display order contradict each other, we want to |
| // highlight the part of the name used for sorting. |
| mHighlightWhenScrolling = false; |
| if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY && |
| mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) { |
| mHighlightWhenScrolling = true; |
| } else if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE && |
| mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { |
| mHighlightWhenScrolling = true; |
| } |
| |
| String[] projection = getProjectionForQuery(); |
| if (mSearchMode && TextUtils.isEmpty(getTextFilter())) { |
| mAdapter.changeCursor(new MatrixCursor(projection)); |
| return; |
| } |
| |
| String callingPackage = getCallingPackage(); |
| Uri uri = getUriToQuery(); |
| if (!TextUtils.isEmpty(callingPackage)) { |
| uri = uri.buildUpon() |
| .appendQueryParameter(ContactsContract.REQUESTING_PACKAGE_PARAM_KEY, |
| callingPackage) |
| .build(); |
| } |
| |
| // Kick off the new query |
| switch (mMode) { |
| case MODE_GROUP: |
| case MODE_DEFAULT: |
| case MODE_CUSTOM: |
| case MODE_PICK_CONTACT: |
| case MODE_PICK_OR_CREATE_CONTACT: |
| case MODE_INSERT_OR_EDIT_CONTACT: |
| mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, getContactSelection(), |
| null, getSortOrder(projection)); |
| break; |
| |
| case MODE_LEGACY_PICK_PERSON: |
| case MODE_LEGACY_PICK_OR_CREATE_PERSON: { |
| mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, |
| People.DISPLAY_NAME); |
| break; |
| } |
| case MODE_PICK_POSTAL: |
| case MODE_QUERY: |
| case MODE_QUERY_PICK: |
| case MODE_QUERY_PICK_PHONE: |
| case MODE_QUERY_PICK_TO_VIEW: |
| case MODE_QUERY_PICK_TO_EDIT: { |
| mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, |
| getSortOrder(projection)); |
| break; |
| } |
| |
| case MODE_STARRED: |
| mQueryHandler.startQuery(QUERY_TOKEN, null, uri, |
| projection, Contacts.STARRED + "=1", null, |
| getSortOrder(projection)); |
| break; |
| |
| case MODE_FREQUENT: |
| mQueryHandler.startQuery(QUERY_TOKEN, null, uri, |
| projection, |
| Contacts.TIMES_CONTACTED + " > 0", null, |
| Contacts.TIMES_CONTACTED + " DESC, " |
| + getSortOrder(projection)); |
| break; |
| |
| case MODE_STREQUENT: |
| mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, null); |
| break; |
| |
| case MODE_PICK_PHONE: |
| mQueryHandler.startQuery(QUERY_TOKEN, null, uri, |
| projection, CLAUSE_ONLY_VISIBLE, null, getSortOrder(projection)); |
| break; |
| |
| case MODE_LEGACY_PICK_PHONE: |
| mQueryHandler.startQuery(QUERY_TOKEN, null, uri, |
| projection, null, null, Phones.DISPLAY_NAME); |
| break; |
| |
| case MODE_LEGACY_PICK_POSTAL: |
| mQueryHandler.startQuery(QUERY_TOKEN, null, uri, |
| projection, |
| ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL, null, |
| ContactMethods.DISPLAY_NAME); |
| break; |
| |
| case MODE_JOIN_CONTACT: |
| mQueryHandler.setLoadingJoinSuggestions(true); |
| mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, |
| null, null, null); |
| break; |
| } |
| } |
| |
| /** |
| * Called from a background thread to do the filter and return the resulting cursor. |
| * |
| * @param filter the text that was entered to filter on |
| * @return a cursor with the results of the filter |
| */ |
| Cursor doFilter(String filter) { |
| String[] projection = getProjectionForQuery(); |
| if (mSearchMode && TextUtils.isEmpty(getTextFilter())) { |
| return new MatrixCursor(projection); |
| } |
| |
| final ContentResolver resolver = getContentResolver(); |
| switch (mMode) { |
| case MODE_DEFAULT: |
| case MODE_CUSTOM: |
| case MODE_PICK_CONTACT: |
| case MODE_PICK_OR_CREATE_CONTACT: |
| case MODE_INSERT_OR_EDIT_CONTACT: { |
| return resolver.query(getContactFilterUri(filter), projection, |
| getContactSelection(), null, getSortOrder(projection)); |
| } |
| |
| case MODE_LEGACY_PICK_PERSON: |
| case MODE_LEGACY_PICK_OR_CREATE_PERSON: { |
| return resolver.query(getPeopleFilterUri(filter), projection, null, null, |
| People.DISPLAY_NAME); |
| } |
| |
| case MODE_STARRED: { |
| return resolver.query(getContactFilterUri(filter), projection, |
| Contacts.STARRED + "=1", null, |
| getSortOrder(projection)); |
| } |
| |
| case MODE_FREQUENT: { |
| return resolver.query(getContactFilterUri(filter), projection, |
| Contacts.TIMES_CONTACTED + " > 0", null, |
| Contacts.TIMES_CONTACTED + " DESC, " |
| + getSortOrder(projection)); |
| } |
| |
| case MODE_STREQUENT: { |
| Uri uri; |
| if (!TextUtils.isEmpty(filter)) { |
| uri = Uri.withAppendedPath(Contacts.CONTENT_STREQUENT_FILTER_URI, |
| Uri.encode(filter)); |
| } else { |
| uri = Contacts.CONTENT_STREQUENT_URI; |
| } |
| return resolver.query(uri, projection, null, null, null); |
| } |
| |
| case MODE_PICK_PHONE: { |
| Uri uri = getUriToQuery(); |
| if (!TextUtils.isEmpty(filter)) { |
| uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(filter)); |
| } |
| return resolver.query(uri, projection, CLAUSE_ONLY_VISIBLE, null, |
| getSortOrder(projection)); |
| } |
| |
| case MODE_LEGACY_PICK_PHONE: { |
| //TODO: Support filtering here (bug 2092503) |
| break; |
| } |
| |
| case MODE_JOIN_CONTACT: { |
| |
| // We are on a background thread. Run queries one after the other synchronously |
| Cursor cursor = resolver.query(getJoinSuggestionsUri(filter), projection, null, |
| null, null); |
| mAdapter.setSuggestionsCursor(cursor); |
| mJoinModeShowAllContacts = false; |
| return resolver.query(getContactFilterUri(filter), projection, |
| Contacts._ID + " != " + mQueryAggregateId + " AND " + CLAUSE_ONLY_VISIBLE, |
| null, getSortOrder(projection)); |
| } |
| } |
| throw new UnsupportedOperationException("filtering not allowed in mode " + mMode); |
| } |
| |
| private Cursor getShowAllContactsLabelCursor(String[] projection) { |
| MatrixCursor matrixCursor = new MatrixCursor(projection); |
| Object[] row = new Object[projection.length]; |
| // The only columns we care about is the id |
| row[SUMMARY_ID_COLUMN_INDEX] = JOIN_MODE_SHOW_ALL_CONTACTS_ID; |
| matrixCursor.addRow(row); |
| return matrixCursor; |
| } |
| |
| /** |
| * Calls the currently selected list item. |
| * @return true if the call was initiated, false otherwise |
| */ |
| boolean callSelection() { |
| ListView list = getListView(); |
| if (list.hasFocus()) { |
| Cursor cursor = (Cursor) list.getSelectedItem(); |
| return callContact(cursor); |
| } |
| return false; |
| } |
| |
| boolean callContact(Cursor cursor) { |
| return callOrSmsContact(cursor, false /*call*/); |
| } |
| |
| boolean smsContact(Cursor cursor) { |
| return callOrSmsContact(cursor, true /*sms*/); |
| } |
| |
| /** |
| * Calls the contact which the cursor is point to. |
| * @return true if the call was initiated, false otherwise |
| */ |
| boolean callOrSmsContact(Cursor cursor, boolean sendSms) { |
| if (cursor == null) { |
| return false; |
| } |
| |
| switch (mMode) { |
| case MODE_PICK_PHONE: |
| case MODE_LEGACY_PICK_PHONE: |
| case MODE_QUERY_PICK_PHONE: { |
| String phone = cursor.getString(PHONE_NUMBER_COLUMN_INDEX); |
| if (sendSms) { |
| ContactsUtils.initiateSms(this, phone); |
| } else { |
| ContactsUtils.initiateCall(this, phone); |
| } |
| return true; |
| } |
| |
| case MODE_PICK_POSTAL: |
| case MODE_LEGACY_PICK_POSTAL: { |
| return false; |
| } |
| |
| default: { |
| |
| boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0; |
| if (!hasPhone) { |
| // There is no phone number. |
| signalError(); |
| return false; |
| } |
| |
| String phone = null; |
| Cursor phonesCursor = null; |
| phonesCursor = queryPhoneNumbers(cursor.getLong(SUMMARY_ID_COLUMN_INDEX)); |
| if (phonesCursor == null || phonesCursor.getCount() == 0) { |
| // No valid number |
| signalError(); |
| return false; |
| } else if (phonesCursor.getCount() == 1) { |
| // only one number, call it. |
| phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER)); |
| } else { |
| phonesCursor.moveToPosition(-1); |
| while (phonesCursor.moveToNext()) { |
| if (phonesCursor.getInt(phonesCursor. |
| getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) { |
| // Found super primary, call it. |
| phone = phonesCursor. |
| getString(phonesCursor.getColumnIndex(Phone.NUMBER)); |
| break; |
| } |
| } |
| } |
| |
| if (phone == null) { |
| // Display dialog to choose a number to call. |
| PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog( |
| this, phonesCursor, sendSms, StickyTabs.getTab(getIntent())); |
| phoneDialog.show(); |
| } else { |
| if (sendSms) { |
| ContactsUtils.initiateSms(this, phone); |
| } else { |
| StickyTabs.saveTab(this, getIntent()); |
| ContactsUtils.initiateCall(this, phone); |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| private Cursor queryPhoneNumbers(long contactId) { |
| Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); |
| Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY); |
| |
| Cursor c = getContentResolver().query(dataUri, |
| new String[] {Phone._ID, Phone.NUMBER, Phone.IS_SUPER_PRIMARY, |
| RawContacts.ACCOUNT_TYPE, Phone.TYPE, Phone.LABEL}, |
| Data.MIMETYPE + "=?", new String[] {Phone.CONTENT_ITEM_TYPE}, null); |
| if (c != null) { |
| if (c.moveToFirst()) { |
| return c; |
| } |
| c.close(); |
| } |
| return null; |
| } |
| |
| // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly |
| protected String getQuantityText(int count, int zeroResourceId, int pluralResourceId) { |
| if (count == 0) { |
| return getString(zeroResourceId); |
| } else { |
| String format = getResources().getQuantityText(pluralResourceId, count).toString(); |
| return String.format(format, count); |
| } |
| } |
| |
| /** |
| * Signal an error to the user. |
| */ |
| void signalError() { |
| //TODO play an error beep or something... |
| } |
| |
| Cursor getItemForView(View view) { |
| ListView listView = getListView(); |
| int index = listView.getPositionForView(view); |
| if (index < 0) { |
| return null; |
| } |
| return (Cursor) listView.getAdapter().getItem(index); |
| } |
| |
| private static class QueryHandler extends AsyncQueryHandler { |
| protected final WeakReference<ContactsListActivity> mActivity; |
| protected boolean mLoadingJoinSuggestions = false; |
| |
| public QueryHandler(Context context) { |
| super(context.getContentResolver()); |
| mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context); |
| } |
| |
| public void setLoadingJoinSuggestions(boolean flag) { |
| mLoadingJoinSuggestions = flag; |
| } |
| |
| @Override |
| protected void onQueryComplete(int token, Object cookie, Cursor cursor) { |
| final ContactsListActivity activity = mActivity.get(); |
| if (activity != null && !activity.isFinishing()) { |
| |
| // Whenever we get a suggestions cursor, we need to immediately kick off |
| // another query for the complete list of contacts |
| if (cursor != null && mLoadingJoinSuggestions) { |
| mLoadingJoinSuggestions = false; |
| if (cursor.getCount() > 0) { |
| activity.mAdapter.setSuggestionsCursor(cursor); |
| } else { |
| cursor.close(); |
| activity.mAdapter.setSuggestionsCursor(null); |
| } |
| |
| if (activity.mAdapter.mSuggestionsCursorCount == 0 |
| || !activity.mJoinModeShowAllContacts) { |
| startQuery(QUERY_TOKEN, null, activity.getContactFilterUri( |
| activity.getTextFilter()), |
| CONTACTS_SUMMARY_PROJECTION, |
| Contacts._ID + " != " + activity.mQueryAggregateId |
| + " AND " + CLAUSE_ONLY_VISIBLE, null, |
| activity.getSortOrder(CONTACTS_SUMMARY_PROJECTION)); |
| return; |
| } |
| |
| cursor = activity.getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION); |
| } |
| |
| activity.mAdapter.changeCursor(cursor); |
| |
| // Now that the cursor is populated again, it's possible to restore the list state |
| if (activity.mListState != null) { |
| activity.mList.onRestoreInstanceState(activity.mListState); |
| activity.mListState = null; |
| } |
| } else { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| } |
| } |
| |
| final static class ContactListItemCache { |
| public CharArrayBuffer nameBuffer = new CharArrayBuffer(128); |
| public CharArrayBuffer dataBuffer = new CharArrayBuffer(128); |
| public CharArrayBuffer highlightedTextBuffer = new CharArrayBuffer(128); |
| public TextWithHighlighting textWithHighlighting; |
| public CharArrayBuffer phoneticNameBuffer = new CharArrayBuffer(128); |
| } |
| |
| final static class PinnedHeaderCache { |
| public TextView titleView; |
| public ColorStateList textColor; |
| public Drawable background; |
| } |
| |
| private final class ContactItemListAdapter extends CursorAdapter |
| implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter { |
| private SectionIndexer mIndexer; |
| private boolean mLoading = true; |
| private CharSequence mUnknownNameText; |
| private boolean mDisplayPhotos = false; |
| private boolean mDisplayCallButton = false; |
| private boolean mDisplayAdditionalData = true; |
| private int mFrequentSeparatorPos = ListView.INVALID_POSITION; |
| private boolean mDisplaySectionHeaders = true; |
| private Cursor mSuggestionsCursor; |
| private int mSuggestionsCursorCount; |
| |
| public ContactItemListAdapter(Context context) { |
| super(context, null, false); |
| |
| mUnknownNameText = context.getText(android.R.string.unknownName); |
| switch (mMode) { |
| case MODE_LEGACY_PICK_POSTAL: |
| case MODE_PICK_POSTAL: |
| case MODE_LEGACY_PICK_PHONE: |
| case MODE_PICK_PHONE: |
| case MODE_STREQUENT: |
| case MODE_FREQUENT: |
| mDisplaySectionHeaders = false; |
| break; |
| } |
| |
| if (mSearchMode) { |
| mDisplaySectionHeaders = false; |
| } |
| |
| // Do not display the second line of text if in a specific SEARCH query mode, usually for |
| // matching a specific E-mail or phone number. Any contact details |
| // shown would be identical, and columns might not even be present |
| // in the returned cursor. |
| if (mMode != MODE_QUERY_PICK_PHONE && mQueryMode != QUERY_MODE_NONE) { |
| mDisplayAdditionalData = false; |
| } |
| |
| if ((mMode & MODE_MASK_NO_DATA) == MODE_MASK_NO_DATA) { |
| mDisplayAdditionalData = false; |
| } |
| |
| if ((mMode & MODE_MASK_SHOW_CALL_BUTTON) == MODE_MASK_SHOW_CALL_BUTTON) { |
| mDisplayCallButton = true; |
| } |
| |
| if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) { |
| mDisplayPhotos = true; |
| } |
| } |
| |
| public boolean getDisplaySectionHeadersEnabled() { |
| return mDisplaySectionHeaders; |
| } |
| |
| public void setSuggestionsCursor(Cursor cursor) { |
| if (mSuggestionsCursor != null) { |
| mSuggestionsCursor.close(); |
| } |
| mSuggestionsCursor = cursor; |
| mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount(); |
| } |
| |
| /** |
| * Callback on the UI thread when the content observer on the backing cursor fires. |
| * Instead of calling requery we need to do an async query so that the requery doesn't |
| * block the UI thread for a long time. |
| */ |
| @Override |
| protected void onContentChanged() { |
| CharSequence constraint = getTextFilter(); |
| if (!TextUtils.isEmpty(constraint)) { |
| // Reset the filter state then start an async filter operation |
| Filter filter = getFilter(); |
| filter.filter(constraint); |
| } else { |
| // Start an async query |
| startQuery(); |
| } |
| } |
| |
| public void setLoading(boolean loading) { |
| mLoading = loading; |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| if (mProviderStatus != ProviderStatus.STATUS_NORMAL) { |
| return true; |
| } |
| |
| if (mSearchMode) { |
| return TextUtils.isEmpty(getTextFilter()); |
| } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) { |
| // This mode mask adds a header and we always want it to show up, even |
| // if the list is empty, so always claim the list is not empty. |
| return false; |
| } else { |
| if (mCursor == null || mLoading) { |
| // We don't want the empty state to show when loading. |
| return false; |
| } else { |
| return super.isEmpty(); |
| } |
| } |
| } |
| |
| @Override |
| public int getItemViewType(int position) { |
| if (position == 0 && (mShowNumberOfContacts || (mMode & MODE_MASK_CREATE_NEW) != 0)) { |
| return IGNORE_ITEM_VIEW_TYPE; |
| } |
| |
| if (isShowAllContactsItemPosition(position)) { |
| return IGNORE_ITEM_VIEW_TYPE; |
| } |
| |
| if (isSearchAllContactsItemPosition(position)) { |
| return IGNORE_ITEM_VIEW_TYPE; |
| } |
| |
| if (getSeparatorId(position) != 0) { |
| // We don't want the separator view to be recycled. |
| return IGNORE_ITEM_VIEW_TYPE; |
| } |
| |
| return super.getItemViewType(position); |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| if (!mDataValid) { |
| throw new IllegalStateException( |
| "this should only be called when the cursor is valid"); |
| } |
| |
| // handle the total contacts item |
| if (position == 0 && mShowNumberOfContacts) { |
| return getTotalContactCountView(parent); |
| } |
| |
| if (position == 0 && (mMode & MODE_MASK_CREATE_NEW) != 0) { |
| // Add the header for creating a new contact |
| return getLayoutInflater().inflate(R.layout.create_new_contact, parent, false); |
| } |
| |
| if (isShowAllContactsItemPosition(position)) { |
| return getLayoutInflater(). |
| inflate(R.layout.contacts_list_show_all_item, parent, false); |
| } |
| |
| if (isSearchAllContactsItemPosition(position)) { |
| return getLayoutInflater(). |
| inflate(R.layout.contacts_list_search_all_item, parent, false); |
| } |
| |
| // Handle the separator specially |
| int separatorId = getSeparatorId(position); |
| if (separatorId != 0) { |
| TextView view = (TextView) getLayoutInflater(). |
| inflate(R.layout.list_separator, parent, false); |
| view.setText(separatorId); |
| return view; |
| } |
| |
| boolean showingSuggestion; |
| Cursor cursor; |
| if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) { |
| showingSuggestion = true; |
| cursor = mSuggestionsCursor; |
| } else { |
| showingSuggestion = false; |
| cursor = mCursor; |
| } |
| |
| int realPosition = getRealPosition(position); |
| if (!cursor.moveToPosition(realPosition)) { |
| throw new IllegalStateException("couldn't move cursor to position " + position); |
| } |
| |
| boolean newView; |
| View v; |
| if (convertView == null || convertView.getTag() == null) { |
| newView = true; |
| v = newView(mContext, cursor, parent); |
| } else { |
| newView = false; |
| v = convertView; |
| } |
| bindView(v, mContext, cursor); |
| bindSectionHeader(v, realPosition, mDisplaySectionHeaders && !showingSuggestion); |
| return v; |
| } |
| |
| private View getTotalContactCountView(ViewGroup parent) { |
| final LayoutInflater inflater = getLayoutInflater(); |
| View view = inflater.inflate(R.layout.total_contacts, parent, false); |
| |
| TextView totalContacts = (TextView) view.findViewById(R.id.totalContactsText); |
| |
| String text; |
| int count = getRealCount(); |
| |
| if (mSearchMode && !TextUtils.isEmpty(getTextFilter())) { |
| text = getQuantityText(count, R.string.listFoundAllContactsZero, |
| R.plurals.searchFoundContacts); |
| } else { |
| if (mDisplayOnlyPhones) { |
| text = getQuantityText(count, R.string.listTotalPhoneContactsZero, |
| R.plurals.listTotalPhoneContacts); |
| } else { |
| text = getQuantityText(count, R.string.listTotalAllContactsZero, |
| R.plurals.listTotalAllContacts); |
| } |
| } |
| totalContacts.setText(text); |
| return view; |
| } |
| |
| private boolean isShowAllContactsItemPosition(int position) { |
| return mMode == MODE_JOIN_CONTACT && mJoinModeShowAllContacts |
| && mSuggestionsCursorCount != 0 && position == mSuggestionsCursorCount + 2; |
| } |
| |
| private boolean isSearchAllContactsItemPosition(int position) { |
| return mSearchMode && position == getCount() - 1; |
| } |
| |
| private int getSeparatorId(int position) { |
| int separatorId = 0; |
| if (position == mFrequentSeparatorPos) { |
| separatorId = R.string.favoritesFrquentSeparator; |
| } |
| if (mSuggestionsCursorCount != 0) { |
| if (position == 0) { |
| separatorId = R.string.separatorJoinAggregateSuggestions; |
| } else if (position == mSuggestionsCursorCount + 1) { |
| separatorId = R.string.separatorJoinAggregateAll; |
| } |
| } |
| return separatorId; |
| } |
| |
| @Override |
| public View newView(Context context, Cursor cursor, ViewGroup parent) { |
| final ContactListItemView view = new ContactListItemView(context, null); |
| view.setOnCallButtonClickListener(ContactsListActivity.this); |
| view.setTag(new ContactListItemCache()); |
| return view; |
| } |
| |
| @Override |
| public void bindView(View itemView, Context context, Cursor cursor) { |
| final ContactListItemView view = (ContactListItemView)itemView; |
| final ContactListItemCache cache = (ContactListItemCache) view.getTag(); |
| |
| int typeColumnIndex; |
| int dataColumnIndex; |
| int labelColumnIndex; |
| int defaultType; |
| int nameColumnIndex; |
| int phoneticNameColumnIndex; |
| boolean displayAdditionalData = mDisplayAdditionalData; |
| boolean highlightingEnabled = false; |
| switch(mMode) { |
| case MODE_PICK_PHONE: |
| case MODE_LEGACY_PICK_PHONE: |
| case MODE_QUERY_PICK_PHONE: { |
| nameColumnIndex = PHONE_DISPLAY_NAME_COLUMN_INDEX; |
| phoneticNameColumnIndex = -1; |
| dataColumnIndex = PHONE_NUMBER_COLUMN_INDEX; |
| typeColumnIndex = PHONE_TYPE_COLUMN_INDEX; |
| labelColumnIndex = PHONE_LABEL_COLUMN_INDEX; |
| defaultType = Phone.TYPE_HOME; |
| break; |
| } |
| case MODE_PICK_POSTAL: |
| case MODE_LEGACY_PICK_POSTAL: { |
| nameColumnIndex = POSTAL_DISPLAY_NAME_COLUMN_INDEX; |
| phoneticNameColumnIndex = -1; |
| dataColumnIndex = POSTAL_ADDRESS_COLUMN_INDEX; |
| typeColumnIndex = POSTAL_TYPE_COLUMN_INDEX; |
| labelColumnIndex = POSTAL_LABEL_COLUMN_INDEX; |
| defaultType = StructuredPostal.TYPE_HOME; |
| break; |
| } |
| default: { |
| nameColumnIndex = getSummaryDisplayNameColumnIndex(); |
| if (mMode == MODE_LEGACY_PICK_PERSON |
| || mMode == MODE_LEGACY_PICK_OR_CREATE_PERSON) { |
| phoneticNameColumnIndex = -1; |
| } else { |
| phoneticNameColumnIndex = SUMMARY_PHONETIC_NAME_COLUMN_INDEX; |
| } |
| dataColumnIndex = -1; |
| typeColumnIndex = -1; |
| labelColumnIndex = -1; |
| defaultType = Phone.TYPE_HOME; |
| displayAdditionalData = false; |
| highlightingEnabled = mHighlightWhenScrolling && mMode != MODE_STREQUENT; |
| } |
| } |
| |
| // Set the name |
| cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer); |
| TextView nameView = view.getNameTextView(); |
| int size = cache.nameBuffer.sizeCopied; |
| if (size != 0) { |
| if (highlightingEnabled) { |
| if (cache.textWithHighlighting == null) { |
| cache.textWithHighlighting = |
| mHighlightingAnimation.createTextWithHighlighting(); |
| } |
| buildDisplayNameWithHighlighting(nameView, cursor, cache.nameBuffer, |
| cache.highlightedTextBuffer, cache.textWithHighlighting); |
| } else { |
| nameView.setText(cache.nameBuffer.data, 0, size); |
| } |
| } else { |
| nameView.setText(mUnknownNameText); |
| } |
| |
| boolean hasPhone = cursor.getColumnCount() >= SUMMARY_HAS_PHONE_COLUMN_INDEX |
| && cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0; |
| |
| // Make the call button visible if requested. |
| if (mDisplayCallButton && hasPhone) { |
| int pos = cursor.getPosition(); |
| view.showCallButton(android.R.id.button1, pos); |
| } else { |
| view.hideCallButton(); |
| } |
| |
| // Set the photo, if requested |
| if (mDisplayPhotos) { |
| boolean useQuickContact = (mMode & MODE_MASK_DISABLE_QUIKCCONTACT) == 0; |
| |
| long photoId = 0; |
| if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) { |
| photoId = cursor.getLong(SUMMARY_PHOTO_ID_COLUMN_INDEX); |
| } |
| |
| ImageView viewToUse; |
| if (useQuickContact) { |
| // Build soft lookup reference |
| final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX); |
| final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX); |
| QuickContactBadge quickContact = view.getQuickContact(); |
| quickContact.assignContactUri(Contacts.getLookupUri(contactId, lookupKey)); |
| quickContact.setSelectedContactsAppTabIndex(StickyTabs.getTab(getIntent())); |
| viewToUse = quickContact; |
| } else { |
| viewToUse = view.getPhotoView(); |
| } |
| |
| final int position = cursor.getPosition(); |
| mPhotoLoader.loadPhoto(viewToUse, photoId); |
| } |
| |
| if ((mMode & MODE_MASK_NO_PRESENCE) == 0) { |
| // Set the proper icon (star or presence or nothing) |
| int serverStatus; |
| if (!cursor.isNull(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX)) { |
| serverStatus = cursor.getInt(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX); |
| Drawable icon = ContactPresenceIconUtil.getPresenceIcon(mContext, serverStatus); |
| if (icon != null) { |
| view.setPresence(icon); |
| } else { |
| view.setPresence(null); |
| } |
| } else { |
| view.setPresence(null); |
| } |
| } else { |
| view.setPresence(null); |
| } |
| |
| if (mShowSearchSnippets) { |
| boolean showSnippet = false; |
| String snippetMimeType = cursor.getString(SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX); |
| if (Email.CONTENT_ITEM_TYPE.equals(snippetMimeType)) { |
| String email = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX); |
| if (!TextUtils.isEmpty(email)) { |
| view.setSnippet(email); |
| showSnippet = true; |
| } |
| } else if (Organization.CONTENT_ITEM_TYPE.equals(snippetMimeType)) { |
| String company = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX); |
| String title = cursor.getString(SUMMARY_SNIPPET_DATA4_COLUMN_INDEX); |
| if (!TextUtils.isEmpty(company)) { |
| if (!TextUtils.isEmpty(title)) { |
| view.setSnippet(company + " / " + title); |
| } else { |
| view.setSnippet(company); |
| } |
| showSnippet = true; |
| } else if (!TextUtils.isEmpty(title)) { |
| view.setSnippet(title); |
| showSnippet = true; |
| } |
| } else if (Nickname.CONTENT_ITEM_TYPE.equals(snippetMimeType)) { |
| String nickname = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX); |
| if (!TextUtils.isEmpty(nickname)) { |
| view.setSnippet(nickname); |
| showSnippet = true; |
| } |
| } |
| |
| if (!showSnippet) { |
| view.setSnippet(null); |
| } |
| } |
| |
| if (!displayAdditionalData) { |
| if (phoneticNameColumnIndex != -1) { |
| |
| // Set the name |
| cursor.copyStringToBuffer(phoneticNameColumnIndex, cache.phoneticNameBuffer); |
| int phoneticNameSize = cache.phoneticNameBuffer.sizeCopied; |
| if (phoneticNameSize != 0) { |
| view.setLabel(cache.phoneticNameBuffer.data, phoneticNameSize); |
| } else { |
| view.setLabel(null); |
| } |
| } else { |
| view.setLabel(null); |
| } |
| return; |
| } |
| |
| // Set the data. |
| cursor.copyStringToBuffer(dataColumnIndex, cache.dataBuffer); |
| |
| size = cache.dataBuffer.sizeCopied; |
| view.setData(cache.dataBuffer.data, size); |
| |
| // Set the label. |
| if (!cursor.isNull(typeColumnIndex)) { |
| final int type = cursor.getInt(typeColumnIndex); |
| final String label = cursor.getString(labelColumnIndex); |
| |
| if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) { |
| // TODO cache |
| view.setLabel(StructuredPostal.getTypeLabel(context.getResources(), type, |
| label)); |
| } else { |
| // TODO cache |
| view.setLabel(Phone.getTypeLabel(context.getResources(), type, label)); |
| } |
| } else { |
| view.setLabel(null); |
| } |
| } |
| |
| /** |
| * Computes the span of the display name that has highlighted parts and configures |
| * the display name text view accordingly. |
| */ |
| private void buildDisplayNameWithHighlighting(TextView textView, Cursor cursor, |
| CharArrayBuffer buffer1, CharArrayBuffer buffer2, |
| TextWithHighlighting textWithHighlighting) { |
| int oppositeDisplayOrderColumnIndex; |
| if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { |
| oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX; |
| } else { |
| oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX; |
| } |
| cursor.copyStringToBuffer(oppositeDisplayOrderColumnIndex, buffer2); |
| |
| textWithHighlighting.setText(buffer1, buffer2); |
| textView.setText(textWithHighlighting); |
| } |
| |
| private void bindSectionHeader(View itemView, int position, boolean displaySectionHeaders) { |
| final ContactListItemView view = (ContactListItemView)itemView; |
| final ContactListItemCache cache = (ContactListItemCache) view.getTag(); |
| if (!displaySectionHeaders) { |
| view.setSectionHeader(null); |
| view.setDividerVisible(true); |
| } else { |
| final int section = getSectionForPosition(position); |
| if (getPositionForSection(section) == position) { |
| String title = (String)mIndexer.getSections()[section]; |
| view.setSectionHeader(title); |
| } else { |
| view.setDividerVisible(false); |
| view.setSectionHeader(null); |
| } |
| |
| // move the divider for the last item in a section |
| if (getPositionForSection(section + 1) - 1 == position) { |
| view.setDividerVisible(false); |
| } else { |
| view.setDividerVisible(true); |
| } |
| } |
| } |
| |
| @Override |
| public void changeCursor(Cursor cursor) { |
| if (cursor != null) { |
| setLoading(false); |
| } |
| |
| // Get the split between starred and frequent items, if the mode is strequent |
| mFrequentSeparatorPos = ListView.INVALID_POSITION; |
| int cursorCount = 0; |
| if (cursor != null && (cursorCount = cursor.getCount()) > 0 |
| && mMode == MODE_STREQUENT) { |
| cursor.move(-1); |
| for (int i = 0; cursor.moveToNext(); i++) { |
| int starred = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX); |
| if (starred == 0) { |
| if (i > 0) { |
| // Only add the separator when there are starred items present |
| mFrequentSeparatorPos = i; |
| } |
| break; |
| } |
| } |
| } |
| |
| if (cursor != null && mSearchResultsMode) { |
| TextView foundContactsText = (TextView)findViewById(R.id.search_results_found); |
| String text = getQuantityText(cursor.getCount(), R.string.listFoundAllContactsZero, |
| R.plurals.listFoundAllContacts); |
| foundContactsText.setText(text); |
| } |
| |
| super.changeCursor(cursor); |
| // Update the indexer for the fast scroll widget |
| updateIndexer(cursor); |
| } |
| |
| private void updateIndexer(Cursor cursor) { |
| if (cursor == null) { |
| mIndexer = null; |
| return; |
| } |
| |
| Bundle bundle = cursor.getExtras(); |
| if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) { |
| String sections[] = |
| bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); |
| int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); |
| mIndexer = new ContactsSectionIndexer(sections, counts); |
| } else { |
| mIndexer = null; |
| } |
| } |
| |
| /** |
| * Run the query on a helper thread. Beware that this code does not run |
| * on the main UI thread! |
| */ |
| @Override |
| public Cursor runQueryOnBackgroundThread(CharSequence constraint) { |
| return doFilter(constraint.toString()); |
| } |
| |
| public Object [] getSections() { |
| if (mIndexer == null) { |
| return new String[] { " " }; |
| } else { |
| return mIndexer.getSections(); |
| } |
| } |
| |
| public int getPositionForSection(int sectionIndex) { |
| if (mIndexer == null) { |
| return -1; |
| } |
| |
| return mIndexer.getPositionForSection(sectionIndex); |
| } |
| |
| public int getSectionForPosition(int position) { |
| if (mIndexer == null) { |
| return -1; |
| } |
| |
| return mIndexer.getSectionForPosition(position); |
| } |
| |
| @Override |
| public boolean areAllItemsEnabled() { |
| return mMode != MODE_STARRED |
| && !mShowNumberOfContacts |
| && mSuggestionsCursorCount == 0; |
| } |
| |
| @Override |
| public boolean isEnabled(int position) { |
| if (mShowNumberOfContacts) { |
| if (position == 0) { |
| return false; |
| } |
| position--; |
| } |
| |
| if (mSuggestionsCursorCount > 0) { |
| return position != 0 && position != mSuggestionsCursorCount + 1; |
| } |
| return position != mFrequentSeparatorPos; |
| } |
| |
| @Override |
| public int getCount() { |
| if (!mDataValid) { |
| return 0; |
| } |
| int superCount = super.getCount(); |
| |
| if (mShowNumberOfContacts && (mSearchMode || superCount > 0)) { |
| // We don't want to count this header if it's the only thing visible, so that |
| // the empty text will display. |
| superCount++; |
| } |
| |
| if (mSearchMode) { |
| // Last element in the list is the "Find |
| superCount++; |
| } |
| |
| // We do not show the "Create New" button in Search mode |
| if ((mMode & MODE_MASK_CREATE_NEW) != 0 && !mSearchMode) { |
| // Count the "Create new contact" line |
| superCount++; |
| } |
| |
| if (mSuggestionsCursorCount != 0) { |
| // When showing suggestions, we have 2 additional list items: the "Suggestions" |
| // and "All contacts" headers. |
| return mSuggestionsCursorCount + superCount + 2; |
| } |
| else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) { |
| // When showing strequent list, we have an additional list item - the separator. |
| return superCount + 1; |
| } else { |
| return superCount; |
| } |
| } |
| |
| /** |
| * Gets the actual count of contacts and excludes all the headers. |
| */ |
| public int getRealCount() { |
| return super.getCount(); |
| } |
| |
| private int getRealPosition(int pos) { |
| if (mShowNumberOfContacts) { |
| pos--; |
| } |
| |
| if ((mMode & MODE_MASK_CREATE_NEW) != 0 && !mSearchMode) { |
| return pos - 1; |
| } else if (mSuggestionsCursorCount != 0) { |
| // When showing suggestions, we have 2 additional list items: the "Suggestions" |
| // and "All contacts" separators. |
| if (pos < mSuggestionsCursorCount + 2) { |
| // We are in the upper partition (Suggestions). Adjusting for the "Suggestions" |
| // separator. |
| return pos - 1; |
| } else { |
| // We are in the lower partition (All contacts). Adjusting for the size |
| // of the upper partition plus the two separators. |
| return pos - mSuggestionsCursorCount - 2; |
| } |
| } else if (mFrequentSeparatorPos == ListView.INVALID_POSITION) { |
| // No separator, identity map |
| return pos; |
| } else if (pos <= mFrequentSeparatorPos) { |
| // Before or at the separator, identity map |
| return pos; |
| } else { |
| // After the separator, remove 1 from the pos to get the real underlying pos |
| return pos - 1; |
| } |
| } |
| |
| @Override |
| public Object getItem(int pos) { |
| if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) { |
| mSuggestionsCursor.moveToPosition(getRealPosition(pos)); |
| return mSuggestionsCursor; |
| } else if (isSearchAllContactsItemPosition(pos)){ |
| return null; |
| } else { |
| int realPosition = getRealPosition(pos); |
| if (realPosition < 0) { |
| return null; |
| } |
| return super.getItem(realPosition); |
| } |
| } |
| |
| @Override |
| public long getItemId(int pos) { |
| if (mSuggestionsCursorCount != 0 && pos < mSuggestionsCursorCount + 2) { |
| if (mSuggestionsCursor.moveToPosition(pos - 1)) { |
| return mSuggestionsCursor.getLong(mRowIDColumn); |
| } else { |
| return 0; |
| } |
| } else if (isSearchAllContactsItemPosition(pos)) { |
| return 0; |
| } |
| int realPosition = getRealPosition(pos); |
| if (realPosition < 0) { |
| return 0; |
| } |
| return super.getItemId(realPosition); |
| } |
| |
| public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, |
| int totalItemCount) { |
| if (view instanceof PinnedHeaderListView) { |
| ((PinnedHeaderListView)view).configureHeaderView(firstVisibleItem); |
| } |
| } |
| |
| public void onScrollStateChanged(AbsListView view, int scrollState) { |
| if (mHighlightWhenScrolling) { |
| if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) { |
| mHighlightingAnimation.startHighlighting(); |
| } else { |
| mHighlightingAnimation.stopHighlighting(); |
| } |
| } |
| |
| if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { |
| mPhotoLoader.pause(); |
| } else if (mDisplayPhotos) { |
| mPhotoLoader.resume(); |
| } |
| } |
| |
| /** |
| * Computes the state of the pinned header. It can be invisible, fully |
| * visible or partially pushed up out of the view. |
| */ |
| public int getPinnedHeaderState(int position) { |
| if (mIndexer == null || mCursor == null || mCursor.getCount() == 0) { |
| return PINNED_HEADER_GONE; |
| } |
| |
| int realPosition = getRealPosition(position); |
| if (realPosition < 0) { |
| return PINNED_HEADER_GONE; |
| } |
| |
| // The header should get pushed up if the top item shown |
| // is the last item in a section for a particular letter. |
| int section = getSectionForPosition(realPosition); |
| int nextSectionPosition = getPositionForSection(section + 1); |
| if (nextSectionPosition != -1 && realPosition == nextSectionPosition - 1) { |
| return PINNED_HEADER_PUSHED_UP; |
| } |
| |
| return PINNED_HEADER_VISIBLE; |
| } |
| |
| /** |
| * Configures the pinned header by setting the appropriate text label |
| * and also adjusting color if necessary. The color needs to be |
| * adjusted when the pinned header is being pushed up from the view. |
| */ |
| public void configurePinnedHeader(View header, int position, int alpha) { |
| PinnedHeaderCache cache = (PinnedHeaderCache)header.getTag(); |
| if (cache == null) { |
| cache = new PinnedHeaderCache(); |
| cache.titleView = (TextView)header.findViewById(R.id.header_text); |
| cache.textColor = cache.titleView.getTextColors(); |
| cache.background = header.getBackground(); |
| header.setTag(cache); |
| } |
| |
| int realPosition = getRealPosition(position); |
| int section = getSectionForPosition(realPosition); |
| |
| String title = (String)mIndexer.getSections()[section]; |
| cache.titleView.setText(title); |
| |
| if (alpha == 255) { |
| // Opaque: use the default background, and the original text color |
| header.setBackgroundDrawable(cache.background); |
| cache.titleView.setTextColor(cache.textColor); |
| } else { |
| // Faded: use a solid color approximation of the background, and |
| // a translucent text color |
| header.setBackgroundColor(Color.rgb( |
| Color.red(mPinnedHeaderBackgroundColor) * alpha / 255, |
| Color.green(mPinnedHeaderBackgroundColor) * alpha / 255, |
| Color.blue(mPinnedHeaderBackgroundColor) * alpha / 255)); |
| |
| int textColor = cache.textColor.getDefaultColor(); |
| cache.titleView.setTextColor(Color.argb(alpha, |
| Color.red(textColor), Color.green(textColor), Color.blue(textColor))); |
| } |
| } |
| } |
| |
| private ContactsPreferences.ChangeListener mPreferencesChangeListener = |
| new ContactsPreferences.ChangeListener() { |
| @Override |
| public void onChange() { |
| // When returning from DisplayOptions, onActivityResult ensures that we reload the list, |
| // so we do not have to do anything here. However, ContactsPreferences requires a change |
| // listener, otherwise it would not reload its settings. |
| } |
| }; |
| } |