/* * 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.list; import com.android.common.widget.CompositeCursorAdapter.Partition; import com.android.contacts.ContactListEmptyView; import com.android.contacts.ContactPhotoManager; import com.android.contacts.R; import com.android.contacts.preference.ContactsPreferences; import com.android.contacts.widget.ContextMenuAdapter; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Activity; import android.app.Fragment; import android.app.LoaderManager; import android.app.LoaderManager.LoaderCallbacks; import android.content.ContentResolver; import android.content.Context; import android.content.CursorLoader; import android.content.IContentService; 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.os.RemoteException; import android.provider.ContactsContract; import android.provider.ContactsContract.Directory; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; 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 android.widget.TextView; /** * 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 ContextMenuAdapter mContextMenuAdapter; private ContactPhotoManager mPhotoManager; private ContactListEmptyView mEmptyView; private ProviderStatusLoader mProviderStatusLoader; 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; } public ContactListEmptyView getEmptyView() { return mEmptyView; } @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); if (mProviderStatusLoader == null) { mProviderStatusLoader = new ProviderStatusLoader(mContext); } 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); mAdapter.configureDirectoryLoader(loader); 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(); } /** * Configures the empty view. It is called when we are about to populate * the list with an empty cursor. */ protected void prepareEmptyView() { } /** * 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 (except the default one). int count = mAdapter.getPartitionCount(); for (int i = count; --i >= 1;) { mAdapter.removePartition(i); } } 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; } public void setContextMenuAdapter(ContextMenuAdapter adapter) { mContextMenuAdapter = adapter; if (mListView != null) { mListView.setOnCreateContextMenuListener(adapter); } } public ContextMenuAdapter getContextMenuAdapter() { return mContextMenuAdapter; } 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(com.android.internal.R.id.empty); if (emptyView != null) { mListView.setEmptyView(emptyView); if (emptyView instanceof ContactListEmptyView) { mEmptyView = (ContactListEmptyView)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); if (mContextMenuAdapter != null) { mListView.setOnCreateContextMenuListener(mContextMenuAdapter); } 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(); } /** * Dismisses the search UI along with the keyboard if the filter text is empty. */ public void onClose() { hideSoftKeyboard(); finish(); } /** * Restore the list state after the adapter is populated. */ protected void completeRestoreInstanceState() { if (mListState != null) { mListView.onRestoreInstanceState(mListState); mListState = null; } } protected void setEmptyText(int resourceId) { TextView empty = (TextView) getEmptyView().findViewById(R.id.emptyText); empty.setText(mContext.getText(resourceId)); empty.setVisibility(View.VISIBLE); } // TODO redesign into an async task or loader protected boolean isSyncActive() { Account[] accounts = AccountManager.get(mContext).getAccounts(); if (accounts != null && accounts.length > 0) { IContentService contentService = ContentResolver.getContentService(); for (Account account : accounts) { try { if (contentService.isSyncActive(account, ContactsContract.AUTHORITY)) { return true; } } catch (RemoteException e) { Log.e(TAG, "Could not get the sync status"); } } } return false; } protected boolean hasIccCard() { TelephonyManager telephonyManager = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); return telephonyManager.hasIccCard(); } 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(); } }; }