| /* |
| * 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.LoaderManager; |
| import android.app.LoaderManager.LoaderCallbacks; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.CursorLoader; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.Loader; |
| import android.content.res.Resources; |
| 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.R; |
| import com.android.contacts.common.ContactPhotoManager; |
| import com.android.contacts.common.preference.ContactsPreferences; |
| import com.android.contacts.common.util.ContactListViewUtils; |
| import com.android.contacts.common.util.SchedulingUtils; |
| import com.android.dialerbind.analytics.AnalyticsFragment; |
| import com.android.internal.telephony.TelephonyIntents; |
| |
| import java.util.Locale; |
| |
| /** |
| * Common base class for various contact-related list fragments. |
| */ |
| public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter> |
| extends AnalyticsFragment |
| 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_ADJUST_SELECTION_BOUNDS_ENABLED = |
| "adjustSelectionBoundsEnabled"; |
| 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 mAdjustSelectionBoundsEnabled = true; |
| private boolean mIncludeProfile; |
| private boolean mSearchMode; |
| private boolean mVisibleScrollbarEnabled; |
| private boolean mShowEmptyListForEmptyQuery; |
| private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition(); |
| 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 BroadcastReceiver mSIMStateReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context arg0, Intent arg1) { |
| reloadData(); |
| } |
| }; |
| |
| private Handler mDelayedDirectorySearchHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| if (msg.what == DIRECTORY_SEARCH_MESSAGE) { |
| loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj); |
| } |
| } |
| }; |
| private int defaultVerticalScrollbarPosition; |
| |
| 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_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled); |
| 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); |
| restoreSavedState(savedState); |
| mAdapter = createListAdapter(); |
| mContactsPrefs = new ContactsPreferences(mContext); |
| restoreSavedState(savedState); |
| |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); |
| filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); |
| if (mContext != null) { |
| mContext.registerReceiver(mSIMStateReceiver, filter); |
| } |
| loadPreferences(); |
| } |
| |
| 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); |
| mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_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(mContext); |
| 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(Context context) { |
| return new CursorLoader(context, 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(); |
| |
| 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(); |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| if (mContext != null) { |
| mContext.unregisterReceiver(mSIMStateReceiver); |
| } |
| } |
| |
| protected void reloadData() { |
| removePendingDirectorySearchRequests(); |
| mAdapter.onDataReload(); |
| mLoadPriorityDirectoriesOnly = true; |
| mForceLoad = true; |
| startLoading(); |
| } |
| |
| /** |
| * 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); |
| } |
| } |
| |
| 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 setAdjustSelectionBoundsEnabled(boolean flag) { |
| mAdjustSelectionBoundsEnabled = flag; |
| } |
| |
| public void setIncludeProfile(boolean flag) { |
| mIncludeProfile = flag; |
| if(mAdapter != null) { |
| mAdapter.setIncludeProfile(flag); |
| } |
| } |
| |
| /** |
| * Enter/exit search mode. This is method is tightly related to the current query, and should |
| * 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.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) { |
| if (!TextUtils.equals(mQueryString, queryString)) { |
| if (mShowEmptyListForEmptyQuery && mAdapter != null && mListView != null) { |
| if (TextUtils.isEmpty(mQueryString)) { |
| // Restore the adapter if the query used to be empty. |
| mListView.setAdapter(mAdapter); |
| } else if (TextUtils.isEmpty(queryString)) { |
| // Instantly clear the list view if the new query is empty. |
| mListView.setAdapter(null); |
| } |
| } |
| |
| mQueryString = queryString; |
| setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery); |
| |
| if (mAdapter != null) { |
| mAdapter.setQueryString(queryString); |
| reloadData(); |
| } |
| } |
| } |
| |
| public void setShowEmptyListForNullQuery(boolean show) { |
| mShowEmptyListForEmptyQuery = show; |
| } |
| |
| public int getDirectoryLoaderId() { |
| return DIRECTORY_LOADER_ID; |
| } |
| |
| 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); |
| |
| 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(); |
| |
| getAdapter().setFragmentRootView(getView()); |
| |
| ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, mView); |
| } |
| |
| @Override |
| public void onHiddenChanged(boolean hidden) { |
| super.onHiddenChanged(hidden); |
| if (getActivity() != null && getView() != null && !hidden) { |
| // If the padding was last applied when in a hidden state, it may have been applied |
| // incorrectly. Therefore we need to reapply it. |
| ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, getView()); |
| } |
| } |
| |
| 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.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled); |
| mAdapter.setIncludeProfile(mIncludeProfile); |
| mAdapter.setQueryString(mQueryString); |
| mAdapter.setDirectorySearchMode(mDirectorySearchMode); |
| mAdapter.setPinnedPartitionHeadersEnabled(false); |
| 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(); |
| } |
| }; |
| |
| private int getDefaultVerticalScrollbarPosition() { |
| final Locale locale = Locale.getDefault(); |
| final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); |
| switch (layoutDirection) { |
| case View.LAYOUT_DIRECTION_RTL: |
| return View.SCROLLBAR_POSITION_LEFT; |
| case View.LAYOUT_DIRECTION_LTR: |
| default: |
| return View.SCROLLBAR_POSITION_RIGHT; |
| } |
| } |
| } |