summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--res/layout/contact_detail_list_padding.xml27
-rw-r--r--src/com/android/contacts/common/list/ContactEntryListFragment.java855
-rw-r--r--src/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java41
-rw-r--r--src/com/android/contacts/common/list/PhoneNumberPickerFragment.java304
4 files changed, 1227 insertions, 0 deletions
diff --git a/res/layout/contact_detail_list_padding.xml b/res/layout/contact_detail_list_padding.xml
new file mode 100644
index 00000000..c5dbd06b
--- /dev/null
+++ b/res/layout/contact_detail_list_padding.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!-- The actual padding is embedded in a FrameLayout since we cannot change the
+ visibility of a header view in a ListView without having a parent view. -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <View
+ android:id="@+id/contact_detail_list_padding"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/list_header_extra_top_padding" />
+</FrameLayout>
diff --git a/src/com/android/contacts/common/list/ContactEntryListFragment.java b/src/com/android/contacts/common/list/ContactEntryListFragment.java
new file mode 100644
index 00000000..a6692b8d
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactEntryListFragment.java
@@ -0,0 +1,855 @@
+/*
+ * Copyright (C) 2010 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.common.list;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnFocusChangeListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.android.common.widget.CompositeCursorAdapter.Partition;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.R;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/**
+ * Common base class for various contact-related list fragments.
+ */
+public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter>
+ extends Fragment
+ implements OnItemClickListener, OnScrollListener, OnFocusChangeListener, OnTouchListener,
+ LoaderCallbacks<Cursor> {
+ private static final String TAG = "ContactEntryListFragment";
+
+ // TODO: Make this protected. This should not be used from the PeopleActivity but
+ // instead use the new startActivityWithResultFromFragment API
+ public static final int ACTIVITY_REQUEST_CODE_PICKER = 1;
+
+ private static final String KEY_LIST_STATE = "liststate";
+ private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled";
+ private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled";
+ private static final String KEY_QUICK_CONTACT_ENABLED = "quickContactEnabled";
+ private static final String KEY_INCLUDE_PROFILE = "includeProfile";
+ private static final String KEY_SEARCH_MODE = "searchMode";
+ private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled";
+ private static final String KEY_SCROLLBAR_POSITION = "scrollbarPosition";
+ private static final String KEY_QUERY_STRING = "queryString";
+ private static final String KEY_DIRECTORY_SEARCH_MODE = "directorySearchMode";
+ private static final String KEY_SELECTION_VISIBLE = "selectionVisible";
+ private static final String KEY_REQUEST = "request";
+ private static final String KEY_DARK_THEME = "darkTheme";
+ private static final String KEY_LEGACY_COMPATIBILITY = "legacyCompatibility";
+ private static final String KEY_DIRECTORY_RESULT_LIMIT = "directoryResultLimit";
+
+ private static final String DIRECTORY_ID_ARG_KEY = "directoryId";
+
+ private static final int DIRECTORY_LOADER_ID = -1;
+
+ private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300;
+ private static final int DIRECTORY_SEARCH_MESSAGE = 1;
+
+ private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20;
+
+ private boolean mSectionHeaderDisplayEnabled;
+ private boolean mPhotoLoaderEnabled;
+ private boolean mQuickContactEnabled = true;
+ private boolean mIncludeProfile;
+ private boolean mSearchMode;
+ private boolean mVisibleScrollbarEnabled;
+ private int mVerticalScrollbarPosition = View.SCROLLBAR_POSITION_RIGHT;
+ private String mQueryString;
+ private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE;
+ private boolean mSelectionVisible;
+ private boolean mLegacyCompatibility;
+
+ private boolean mEnabled = true;
+
+ private T mAdapter;
+ private View mView;
+ private ListView mListView;
+
+ /**
+ * Used for keeping track of the scroll state of the list.
+ */
+ private Parcelable mListState;
+
+ private int mDisplayOrder;
+ private int mSortOrder;
+ private int mDirectoryResultLimit = DEFAULT_DIRECTORY_RESULT_LIMIT;
+
+ private ContactPhotoManager mPhotoManager;
+ private ContactsPreferences mContactsPrefs;
+
+ private boolean mForceLoad;
+
+ private boolean mDarkTheme;
+
+ protected boolean mUserProfileExists;
+
+ private static final int STATUS_NOT_LOADED = 0;
+ private static final int STATUS_LOADING = 1;
+ private static final int STATUS_LOADED = 2;
+
+ private int mDirectoryListStatus = STATUS_NOT_LOADED;
+
+ /**
+ * Indicates whether we are doing the initial complete load of data (false) or
+ * a refresh caused by a change notification (true)
+ */
+ private boolean mLoadPriorityDirectoriesOnly;
+
+ private Context mContext;
+
+ private LoaderManager mLoaderManager;
+
+ private Handler mDelayedDirectorySearchHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
+ loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
+ }
+ }
+ };
+
+ protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
+ protected abstract T createListAdapter();
+
+ /**
+ * @param position Please note that the position is already adjusted for
+ * header views, so "0" means the first list item below header
+ * views.
+ */
+ protected abstract void onItemClick(int position, long id);
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ setContext(activity);
+ setLoaderManager(super.getLoaderManager());
+ }
+
+ /**
+ * Sets a context for the fragment in the unit test environment.
+ */
+ public void setContext(Context context) {
+ mContext = context;
+ configurePhotoLoader();
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public void setEnabled(boolean enabled) {
+ if (mEnabled != enabled) {
+ mEnabled = enabled;
+ if (mAdapter != null) {
+ if (mEnabled) {
+ reloadData();
+ } else {
+ mAdapter.clearPartitions();
+ }
+ }
+ }
+ }
+
+ /**
+ * Overrides a loader manager for use in unit tests.
+ */
+ public void setLoaderManager(LoaderManager loaderManager) {
+ mLoaderManager = loaderManager;
+ }
+
+ @Override
+ public LoaderManager getLoaderManager() {
+ return mLoaderManager;
+ }
+
+ public T getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public View getView() {
+ return mView;
+ }
+
+ public ListView getListView() {
+ return mListView;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED, mSectionHeaderDisplayEnabled);
+ outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled);
+ outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled);
+ outState.putBoolean(KEY_INCLUDE_PROFILE, mIncludeProfile);
+ outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
+ outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled);
+ outState.putInt(KEY_SCROLLBAR_POSITION, mVerticalScrollbarPosition);
+ outState.putInt(KEY_DIRECTORY_SEARCH_MODE, mDirectorySearchMode);
+ outState.putBoolean(KEY_SELECTION_VISIBLE, mSelectionVisible);
+ outState.putBoolean(KEY_LEGACY_COMPATIBILITY, mLegacyCompatibility);
+ outState.putString(KEY_QUERY_STRING, mQueryString);
+ outState.putInt(KEY_DIRECTORY_RESULT_LIMIT, mDirectoryResultLimit);
+ outState.putBoolean(KEY_DARK_THEME, mDarkTheme);
+
+ if (mListView != null) {
+ outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState());
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ mContactsPrefs = new ContactsPreferences(mContext);
+ restoreSavedState(savedState);
+ }
+
+ public void restoreSavedState(Bundle savedState) {
+ if (savedState == null) {
+ return;
+ }
+
+ mSectionHeaderDisplayEnabled = savedState.getBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED);
+ mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED);
+ mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED);
+ mIncludeProfile = savedState.getBoolean(KEY_INCLUDE_PROFILE);
+ mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE);
+ mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED);
+ mVerticalScrollbarPosition = savedState.getInt(KEY_SCROLLBAR_POSITION);
+ mDirectorySearchMode = savedState.getInt(KEY_DIRECTORY_SEARCH_MODE);
+ mSelectionVisible = savedState.getBoolean(KEY_SELECTION_VISIBLE);
+ mLegacyCompatibility = savedState.getBoolean(KEY_LEGACY_COMPATIBILITY);
+ mQueryString = savedState.getString(KEY_QUERY_STRING);
+ mDirectoryResultLimit = savedState.getInt(KEY_DIRECTORY_RESULT_LIMIT);
+ mDarkTheme = savedState.getBoolean(KEY_DARK_THEME);
+
+ // Retrieve list state. This will be applied in onLoadFinished
+ mListState = savedState.getParcelable(KEY_LIST_STATE);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
+
+ mForceLoad = loadPreferences();
+
+ mDirectoryListStatus = STATUS_NOT_LOADED;
+ mLoadPriorityDirectoriesOnly = true;
+
+ startLoading();
+ }
+
+ protected void startLoading() {
+ if (mAdapter == null) {
+ // The method was called before the fragment was started
+ return;
+ }
+
+ configureAdapter();
+ int partitionCount = mAdapter.getPartitionCount();
+ for (int i = 0; i < partitionCount; i++) {
+ Partition partition = mAdapter.getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+ if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) {
+ if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) {
+ startLoadingDirectoryPartition(i);
+ }
+ }
+ } else {
+ getLoaderManager().initLoader(i, null, this);
+ }
+ }
+
+ // Next time this method is called, we should start loading non-priority directories
+ mLoadPriorityDirectoriesOnly = false;
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ if (id == DIRECTORY_LOADER_ID) {
+ DirectoryListLoader loader = new DirectoryListLoader(mContext);
+ loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode());
+ loader.setLocalInvisibleDirectoryEnabled(
+ ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED);
+ return loader;
+ } else {
+ CursorLoader loader = createCursorLoader();
+ long directoryId = args != null && args.containsKey(DIRECTORY_ID_ARG_KEY)
+ ? args.getLong(DIRECTORY_ID_ARG_KEY)
+ : Directory.DEFAULT;
+ mAdapter.configureLoader(loader, directoryId);
+ return loader;
+ }
+ }
+
+ public CursorLoader createCursorLoader() {
+ return new CursorLoader(mContext, null, null, null, null, null);
+ }
+
+ private void startLoadingDirectoryPartition(int partitionIndex) {
+ DirectoryPartition partition = (DirectoryPartition)mAdapter.getPartition(partitionIndex);
+ partition.setStatus(DirectoryPartition.STATUS_LOADING);
+ long directoryId = partition.getDirectoryId();
+ if (mForceLoad) {
+ if (directoryId == Directory.DEFAULT) {
+ loadDirectoryPartition(partitionIndex, partition);
+ } else {
+ loadDirectoryPartitionDelayed(partitionIndex, partition);
+ }
+ } else {
+ Bundle args = new Bundle();
+ args.putLong(DIRECTORY_ID_ARG_KEY, directoryId);
+ getLoaderManager().initLoader(partitionIndex, args, this);
+ }
+ }
+
+ /**
+ * Queues up a delayed request to search the specified directory. Since
+ * directory search will likely introduce a lot of network traffic, we want
+ * to wait for a pause in the user's typing before sending a directory request.
+ */
+ private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) {
+ mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition);
+ Message msg = mDelayedDirectorySearchHandler.obtainMessage(
+ DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition);
+ mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS);
+ }
+
+ /**
+ * Loads the directory partition.
+ */
+ protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) {
+ Bundle args = new Bundle();
+ args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId());
+ getLoaderManager().restartLoader(partitionIndex, args, this);
+ }
+
+ /**
+ * Cancels all queued directory loading requests.
+ */
+ private void removePendingDirectorySearchRequests() {
+ mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (!mEnabled) {
+ return;
+ }
+
+ int loaderId = loader.getId();
+ if (loaderId == DIRECTORY_LOADER_ID) {
+ mDirectoryListStatus = STATUS_LOADED;
+ mAdapter.changeDirectories(data);
+ startLoading();
+ } else {
+ onPartitionLoaded(loaderId, data);
+ if (isSearchMode()) {
+ int directorySearchMode = getDirectorySearchMode();
+ if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) {
+ if (mDirectoryListStatus == STATUS_NOT_LOADED) {
+ mDirectoryListStatus = STATUS_LOADING;
+ getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this);
+ } else {
+ startLoading();
+ }
+ }
+ } else {
+ mDirectoryListStatus = STATUS_NOT_LOADED;
+ getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
+ }
+ }
+ }
+
+ public void onLoaderReset(Loader<Cursor> loader) {
+ }
+
+ protected void onPartitionLoaded(int partitionIndex, Cursor data) {
+ if (partitionIndex >= mAdapter.getPartitionCount()) {
+ // When we get unsolicited data, ignore it. This could happen
+ // when we are switching from search mode to the default mode.
+ return;
+ }
+
+ mAdapter.changeCursor(partitionIndex, data);
+ setProfileHeader();
+ showCount(partitionIndex, data);
+
+ if (!isLoading()) {
+ completeRestoreInstanceState();
+ }
+ }
+
+ public boolean isLoading() {
+ if (mAdapter != null && mAdapter.isLoading()) {
+ return true;
+ }
+
+ if (isLoadingDirectoryList()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean isLoadingDirectoryList() {
+ return isSearchMode() && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE
+ && (mDirectoryListStatus == STATUS_NOT_LOADED
+ || mDirectoryListStatus == STATUS_LOADING);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mContactsPrefs.unregisterChangeListener();
+ mAdapter.clearPartitions();
+ }
+
+ protected void reloadData() {
+ removePendingDirectorySearchRequests();
+ mAdapter.onDataReload();
+ mLoadPriorityDirectoriesOnly = true;
+ mForceLoad = true;
+ startLoading();
+ }
+
+ /**
+ * Shows the count of entries included in the list. The default
+ * implementation does nothing.
+ */
+ protected void showCount(int partitionIndex, Cursor data) {
+ }
+
+ /**
+ * Shows a view at the top of the list with a pseudo local profile prompting the user to add
+ * a local profile. Default implementation does nothing.
+ */
+ protected void setProfileHeader() {
+ mUserProfileExists = false;
+ }
+
+ /**
+ * Provides logic that dismisses this fragment. The default implementation
+ * does nothing.
+ */
+ protected void finish() {
+ }
+
+ public void setSectionHeaderDisplayEnabled(boolean flag) {
+ if (mSectionHeaderDisplayEnabled != flag) {
+ mSectionHeaderDisplayEnabled = flag;
+ if (mAdapter != null) {
+ mAdapter.setSectionHeaderDisplayEnabled(flag);
+ }
+ configureVerticalScrollbar();
+ }
+ }
+
+ public boolean isSectionHeaderDisplayEnabled() {
+ return mSectionHeaderDisplayEnabled;
+ }
+
+ public void setVisibleScrollbarEnabled(boolean flag) {
+ if (mVisibleScrollbarEnabled != flag) {
+ mVisibleScrollbarEnabled = flag;
+ configureVerticalScrollbar();
+ }
+ }
+
+ public boolean isVisibleScrollbarEnabled() {
+ return mVisibleScrollbarEnabled;
+ }
+
+ public void setVerticalScrollbarPosition(int position) {
+ if (mVerticalScrollbarPosition != position) {
+ mVerticalScrollbarPosition = position;
+ configureVerticalScrollbar();
+ }
+ }
+
+ private void configureVerticalScrollbar() {
+ boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled();
+
+ if (mListView != null) {
+ mListView.setFastScrollEnabled(hasScrollbar);
+ mListView.setFastScrollAlwaysVisible(hasScrollbar);
+ mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition);
+ mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+ int leftPadding = 0;
+ int rightPadding = 0;
+ if (mVerticalScrollbarPosition == View.SCROLLBAR_POSITION_LEFT) {
+ leftPadding = mContext.getResources().getDimensionPixelOffset(
+ R.dimen.list_visible_scrollbar_padding);
+ } else {
+ rightPadding = mContext.getResources().getDimensionPixelOffset(
+ R.dimen.list_visible_scrollbar_padding);
+ }
+ mListView.setPadding(leftPadding, mListView.getPaddingTop(),
+ rightPadding, mListView.getPaddingBottom());
+ }
+ }
+
+ public void setPhotoLoaderEnabled(boolean flag) {
+ mPhotoLoaderEnabled = flag;
+ configurePhotoLoader();
+ }
+
+ public boolean isPhotoLoaderEnabled() {
+ return mPhotoLoaderEnabled;
+ }
+
+ /**
+ * Returns true if the list is supposed to visually highlight the selected item.
+ */
+ public boolean isSelectionVisible() {
+ return mSelectionVisible;
+ }
+
+ public void setSelectionVisible(boolean flag) {
+ this.mSelectionVisible = flag;
+ }
+
+ public void setQuickContactEnabled(boolean flag) {
+ this.mQuickContactEnabled = flag;
+ }
+
+ public void setIncludeProfile(boolean flag) {
+ mIncludeProfile = flag;
+ if(mAdapter != null) {
+ mAdapter.setIncludeProfile(flag);
+ }
+ }
+
+ /**
+ * Enter/exit search mode. By design, a fragment enters search mode only when it has a
+ * non-empty query text, so the mode must be tightly related to the current query.
+ * For this reason this method must only be called by {@link #setQueryString}.
+ *
+ * Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it.
+ */
+ protected void setSearchMode(boolean flag) {
+ if (mSearchMode != flag) {
+ mSearchMode = flag;
+ setSectionHeaderDisplayEnabled(!mSearchMode);
+
+ if (!flag) {
+ mDirectoryListStatus = STATUS_NOT_LOADED;
+ getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
+ }
+
+ if (mAdapter != null) {
+ mAdapter.setPinnedPartitionHeadersEnabled(flag);
+ mAdapter.setSearchMode(flag);
+
+ mAdapter.clearPartitions();
+ if (!flag) {
+ // If we are switching from search to regular display, remove all directory
+ // partitions after default one, assuming they are remote directories which
+ // should be cleaned up on exiting the search mode.
+ mAdapter.removeDirectoriesAfterDefault();
+ }
+ mAdapter.configureDefaultPartition(false, flag);
+ }
+
+ if (mListView != null) {
+ mListView.setFastScrollEnabled(!flag);
+ }
+ }
+ }
+
+ public final boolean isSearchMode() {
+ return mSearchMode;
+ }
+
+ public final String getQueryString() {
+ return mQueryString;
+ }
+
+ public void setQueryString(String queryString, boolean delaySelection) {
+ // Normalize the empty query.
+ if (TextUtils.isEmpty(queryString)) queryString = null;
+
+ if (!TextUtils.equals(mQueryString, queryString)) {
+ mQueryString = queryString;
+ setSearchMode(!TextUtils.isEmpty(mQueryString));
+
+ if (mAdapter != null) {
+ mAdapter.setQueryString(queryString);
+ reloadData();
+ }
+ }
+ }
+
+ public int getDirectorySearchMode() {
+ return mDirectorySearchMode;
+ }
+
+ public void setDirectorySearchMode(int mode) {
+ mDirectorySearchMode = mode;
+ }
+
+ public boolean isLegacyCompatibilityMode() {
+ return mLegacyCompatibility;
+ }
+
+ public void setLegacyCompatibilityMode(boolean flag) {
+ mLegacyCompatibility = flag;
+ }
+
+ protected int getContactNameDisplayOrder() {
+ return mDisplayOrder;
+ }
+
+ protected void setContactNameDisplayOrder(int displayOrder) {
+ mDisplayOrder = displayOrder;
+ if (mAdapter != null) {
+ mAdapter.setContactNameDisplayOrder(displayOrder);
+ }
+ }
+
+ public int getSortOrder() {
+ return mSortOrder;
+ }
+
+ public void setSortOrder(int sortOrder) {
+ mSortOrder = sortOrder;
+ if (mAdapter != null) {
+ mAdapter.setSortOrder(sortOrder);
+ }
+ }
+
+ public void setDirectoryResultLimit(int limit) {
+ mDirectoryResultLimit = limit;
+ }
+
+ protected boolean loadPreferences() {
+ boolean changed = false;
+ if (getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) {
+ setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
+ changed = true;
+ }
+
+ if (getSortOrder() != mContactsPrefs.getSortOrder()) {
+ setSortOrder(mContactsPrefs.getSortOrder());
+ changed = true;
+ }
+
+ return changed;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ onCreateView(inflater, container);
+
+ mAdapter = createListAdapter();
+
+ boolean searchMode = isSearchMode();
+ mAdapter.setSearchMode(searchMode);
+ mAdapter.configureDefaultPartition(false, searchMode);
+ mAdapter.setPhotoLoader(mPhotoManager);
+ mListView.setAdapter(mAdapter);
+
+ if (!isSearchMode()) {
+ mListView.setFocusableInTouchMode(true);
+ mListView.requestFocus();
+ }
+
+ return mView;
+ }
+
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ mView = inflateView(inflater, container);
+
+ mListView = (ListView)mView.findViewById(android.R.id.list);
+ if (mListView == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is " +
+ "'android.R.id.list'");
+ }
+
+ View emptyView = mView.findViewById(android.R.id.empty);
+ if (emptyView != null) {
+ mListView.setEmptyView(emptyView);
+ }
+
+ mListView.setOnItemClickListener(this);
+ mListView.setOnFocusChangeListener(this);
+ mListView.setOnTouchListener(this);
+ mListView.setFastScrollEnabled(!isSearchMode());
+
+ // 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.
+ mListView.setDividerHeight(0);
+
+ // We manually save/restore the listview state
+ mListView.setSaveEnabled(false);
+
+ configureVerticalScrollbar();
+ configurePhotoLoader();
+ }
+
+ protected void configurePhotoLoader() {
+ if (isPhotoLoaderEnabled() && mContext != null) {
+ if (mPhotoManager == null) {
+ mPhotoManager = ContactPhotoManager.getInstance(mContext);
+ }
+ if (mListView != null) {
+ mListView.setOnScrollListener(this);
+ }
+ if (mAdapter != null) {
+ mAdapter.setPhotoLoader(mPhotoManager);
+ }
+ }
+ }
+
+ protected void configureAdapter() {
+ if (mAdapter == null) {
+ return;
+ }
+
+ mAdapter.setQuickContactEnabled(mQuickContactEnabled);
+ mAdapter.setIncludeProfile(mIncludeProfile);
+ mAdapter.setQueryString(mQueryString);
+ mAdapter.setDirectorySearchMode(mDirectorySearchMode);
+ mAdapter.setPinnedPartitionHeadersEnabled(mSearchMode);
+ mAdapter.setContactNameDisplayOrder(mDisplayOrder);
+ mAdapter.setSortOrder(mSortOrder);
+ mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled);
+ mAdapter.setSelectionVisible(mSelectionVisible);
+ mAdapter.setDirectoryResultLimit(mDirectoryResultLimit);
+ mAdapter.setDarkTheme(mDarkTheme);
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
+ mPhotoManager.pause();
+ } else if (isPhotoLoaderEnabled()) {
+ mPhotoManager.resume();
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ hideSoftKeyboard();
+
+ int adjPosition = position - mListView.getHeaderViewsCount();
+ if (adjPosition >= 0) {
+ onItemClick(adjPosition, id);
+ }
+ }
+
+ private void hideSoftKeyboard() {
+ // Hide soft keyboard, if visible
+ InputMethodManager inputMethodManager = (InputMethodManager)
+ mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0);
+ }
+
+ /**
+ * Dismisses the soft keyboard when the list takes focus.
+ */
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (view == mListView && hasFocus) {
+ hideSoftKeyboard();
+ }
+ }
+
+ /**
+ * Dismisses the soft keyboard when the list is touched.
+ */
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ if (view == mListView) {
+ hideSoftKeyboard();
+ }
+ return false;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ removePendingDirectorySearchRequests();
+ }
+
+ /**
+ * Restore the list state after the adapter is populated.
+ */
+ protected void completeRestoreInstanceState() {
+ if (mListState != null) {
+ mListView.onRestoreInstanceState(mListState);
+ mListState = null;
+ }
+ }
+
+ public void setDarkTheme(boolean value) {
+ mDarkTheme = value;
+ if (mAdapter != null) mAdapter.setDarkTheme(value);
+ }
+
+ /**
+ * Processes a result returned by the contact picker.
+ */
+ public void onPickerResult(Intent data) {
+ throw new UnsupportedOperationException("Picker result handler is not implemented.");
+ }
+
+ private ContactsPreferences.ChangeListener mPreferencesChangeListener =
+ new ContactsPreferences.ChangeListener() {
+ @Override
+ public void onChange() {
+ loadPreferences();
+ reloadData();
+ }
+ };
+}
diff --git a/src/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java b/src/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java
new file mode 100644
index 00000000..9e08fa57
--- /dev/null
+++ b/src/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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.common.list;
+
+import android.app.ActionBar;
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a phone number picker.
+ */
+public interface OnPhoneNumberPickerActionListener {
+
+ /**
+ * Returns the selected phone number to the requester.
+ */
+ void onPickPhoneNumberAction(Uri dataUri);
+
+ /**
+ * Returns the selected number as a shortcut intent.
+ */
+ void onShortcutIntentCreated(Intent intent);
+
+ /**
+ * Called when home menu in {@link ActionBar} is clicked by the user.
+ */
+ void onHomeInActionBarSelected();
+}
diff --git a/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java b/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java
new file mode 100644
index 00000000..ec0eee9c
--- /dev/null
+++ b/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2010 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.common.list;
+
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
+import com.android.contacts.common.util.AccountFilterUtil;
+
+/**
+ * Fragment containing a phone number list for picking.
+ */
+public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter>
+ implements OnShortcutIntentCreatedListener {
+ private static final String TAG = PhoneNumberPickerFragment.class.getSimpleName();
+
+ private static final int REQUEST_CODE_ACCOUNT_FILTER = 1;
+
+ private static final String KEY_SHORTCUT_ACTION = "shortcutAction";
+
+ private OnPhoneNumberPickerActionListener mListener;
+ private String mShortcutAction;
+
+ private ContactListFilter mFilter;
+
+ private View mAccountFilterHeader;
+ /**
+ * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set
+ * to View.GONE.
+ */
+ private View mPaddingView;
+
+ private static final String KEY_FILTER = "filter";
+
+ /** true if the loader has started at least once. */
+ private boolean mLoaderStarted;
+
+ private boolean mUseCallableUri;
+
+ private ContactListItemView.PhotoPosition mPhotoPosition =
+ ContactListItemView.DEFAULT_PHOTO_POSITION;
+
+ private class FilterHeaderClickListener implements OnClickListener {
+ @Override
+ public void onClick(View view) {
+ AccountFilterUtil.startAccountFilterActivityForResult(
+ PhoneNumberPickerFragment.this,
+ REQUEST_CODE_ACCOUNT_FILTER,
+ mFilter);
+ }
+ }
+ private OnClickListener mFilterHeaderClickListener = new FilterHeaderClickListener();
+
+ public PhoneNumberPickerFragment() {
+ setQuickContactEnabled(false);
+ setPhotoLoaderEnabled(true);
+ setSectionHeaderDisplayEnabled(true);
+ setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_DATA_SHORTCUT);
+
+ // Show nothing instead of letting caller Activity show something.
+ setHasOptionsMenu(true);
+ }
+
+ public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) {
+ this.mListener = listener;
+ }
+
+ @Override
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ super.onCreateView(inflater, container);
+
+ View paddingView = inflater.inflate(R.layout.contact_detail_list_padding, null, false);
+ mPaddingView = paddingView.findViewById(R.id.contact_detail_list_padding);
+ getListView().addHeaderView(paddingView);
+
+ mAccountFilterHeader = getView().findViewById(R.id.account_filter_header_container);
+ mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener);
+ updateFilterHeaderView();
+
+ setVisibleScrollbarEnabled(getVisibleScrollbarEnabled());
+ }
+
+ protected boolean getVisibleScrollbarEnabled() {
+ return true;
+ }
+
+ @Override
+ protected void setSearchMode(boolean flag) {
+ super.setSearchMode(flag);
+ updateFilterHeaderView();
+ }
+
+ private void updateFilterHeaderView() {
+ final ContactListFilter filter = getFilter();
+ if (mAccountFilterHeader == null || filter == null) {
+ return;
+ }
+ final boolean shouldShowHeader =
+ !isSearchMode() &&
+ AccountFilterUtil.updateAccountFilterTitleForPhone(
+ mAccountFilterHeader, filter, false);
+ if (shouldShowHeader) {
+ mPaddingView.setVisibility(View.GONE);
+ mAccountFilterHeader.setVisibility(View.VISIBLE);
+ } else {
+ mPaddingView.setVisibility(View.VISIBLE);
+ mAccountFilterHeader.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void restoreSavedState(Bundle savedState) {
+ super.restoreSavedState(savedState);
+
+ if (savedState == null) {
+ return;
+ }
+
+ mFilter = savedState.getParcelable(KEY_FILTER);
+ mShortcutAction = savedState.getString(KEY_SHORTCUT_ACTION);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(KEY_FILTER, mFilter);
+ outState.putString(KEY_SHORTCUT_ACTION, mShortcutAction);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled()
+ if (mListener != null) {
+ mListener.onHomeInActionBarSelected();
+ }
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * @param shortcutAction either {@link Intent#ACTION_CALL} or
+ * {@link Intent#ACTION_SENDTO} or null.
+ */
+ public void setShortcutAction(String shortcutAction) {
+ this.mShortcutAction = shortcutAction;
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ final Uri phoneUri = getPhoneUri(position);
+
+ if (phoneUri != null) {
+ pickPhoneNumber(phoneUri);
+ } else {
+ Log.w(TAG, "Item at " + position + " was clicked before adapter is ready. Ignoring");
+ }
+ }
+
+ protected Uri getPhoneUri(int position) {
+ final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+ return adapter.getDataUri(position);
+ }
+
+ @Override
+ protected void startLoading() {
+ mLoaderStarted = true;
+ super.startLoading();
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ super.onLoadFinished(loader, data);
+
+ // disable scroll bar if there is no data
+ setVisibleScrollbarEnabled(data.getCount() > 0);
+ }
+
+ public void setUseCallableUri(boolean useCallableUri) {
+ mUseCallableUri = useCallableUri;
+ }
+
+ public boolean usesCallableUri() {
+ return mUseCallableUri;
+ }
+
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity());
+ adapter.setDisplayPhotos(true);
+ adapter.setUseCallableUri(mUseCallableUri);
+ return adapter;
+ }
+
+ @Override
+ protected void configureAdapter() {
+ super.configureAdapter();
+
+ final ContactEntryListAdapter adapter = getAdapter();
+ if (adapter == null) {
+ return;
+ }
+
+ if (!isSearchMode() && mFilter != null) {
+ adapter.setFilter(mFilter);
+ }
+
+ setPhotoPosition(adapter);
+ }
+
+ protected void setPhotoPosition(ContactEntryListAdapter adapter) {
+ ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition);
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ return inflater.inflate(R.layout.contact_list_content, null);
+ }
+
+ public void pickPhoneNumber(Uri uri) {
+ if (mShortcutAction == null) {
+ mListener.onPickPhoneNumberAction(uri);
+ } else {
+ startPhoneNumberShortcutIntent(uri);
+ }
+ }
+
+ protected void startPhoneNumberShortcutIntent(Uri uri) {
+ ShortcutIntentBuilder builder = new ShortcutIntentBuilder(getActivity(), this);
+ builder.createPhoneNumberShortcutIntent(uri, mShortcutAction);
+ }
+
+ public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
+ mListener.onShortcutIntentCreated(shortcutIntent);
+ }
+
+ @Override
+ public void onPickerResult(Intent data) {
+ mListener.onPickPhoneNumberAction(data.getData());
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_CODE_ACCOUNT_FILTER) {
+ if (getActivity() != null) {
+ AccountFilterUtil.handleAccountFilterResult(
+ ContactListFilterController.getInstance(getActivity()), resultCode, data);
+ } else {
+ Log.e(TAG, "getActivity() returns null during Fragment#onActivityResult()");
+ }
+ }
+ }
+
+ public ContactListFilter getFilter() {
+ return mFilter;
+ }
+
+ public void setFilter(ContactListFilter filter) {
+ if ((mFilter == null && filter == null) ||
+ (mFilter != null && mFilter.equals(filter))) {
+ return;
+ }
+
+ mFilter = filter;
+ if (mLoaderStarted) {
+ reloadData();
+ }
+ updateFilterHeaderView();
+ }
+
+ public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
+ mPhotoPosition = photoPosition;
+
+ final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+ if (adapter != null) {
+ adapter.setPhotoPosition(photoPosition);
+ }
+ }
+}