diff options
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); + } + } +} |
