/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * 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.silentcircle.contacts.list; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Parcelable; import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.telephony.TelephonyManager; 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 android.widget.TextView; import com.actionbarsherlock.app.SherlockFragment; import com.silentcircle.contacts.ContactPhotoManager; import com.silentcircle.contacts.widget.CompositeCursorAdapter; import com.silentcircle.contacts.ContactListEmptyView; import com.silentcircle.contacts.R; import com.silentcircle.silentcontacts.ScContactsContract.Directory; import com.silentcircle.contacts.preference.ContactsPreferences; /** * Common base class for various contact-related list fragments. */ public abstract class ScContactEntryListFragment<T extends ScContactEntryListAdapter> extends SherlockFragment implements OnItemClickListener, OnScrollListener, OnFocusChangeListener, OnTouchListener, LoaderCallbacks<Cursor> { private static final String TAG = "ScContactEntryListFragment"; // 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 ContactListEmptyView mEmptyView; 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; } public View getOwnView() { 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); 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++) { CompositeCursorAdapter.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) { // TODO DirectoryListLoader loader = new DirectoryListLoader(mContext); // mAdapter.configureDirectoryLoader(loader); // return loader; return null; } 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(); } } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void configureVerticalScrollbar() { boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled(); if (mListView != null) { mListView.setFastScrollEnabled(hasScrollbar); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 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; if (mAdapter != null) mAdapter.setSelectionVisible(mSelectionVisible); } public void setQuickContactEnabled(boolean flag) { this.mQuickContactEnabled = flag; if (mAdapter != null) mAdapter.setQuickContactEnabled(mQuickContactEnabled); } 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(); } mView.setVisibility(View.VISIBLE); 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); 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); 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() { 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(); } }; }