blob: 7073a57ec2bb6af6bec4919ac09a5742825cdea6 [file] [log] [blame]
/*
* 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.
*/
package com.android.contacts.list;
import com.android.contacts.ContactPhotoManager;
import com.android.contacts.ContactTileLoaderFactory;
import com.android.contacts.R;
import com.android.contacts.preference.ContactsPreferences;
import com.android.contacts.util.AccountFilterUtil;
import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.Loader;
import android.database.Cursor;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.Directory;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.FrameLayout;
import android.widget.ListView;
import android.widget.TextView;
/**
* Fragment for Phone UI's favorite screen.
*
* This fragment contains three kinds of contacts in one screen: "starred", "frequent", and "all"
* contacts. To show them at once, this merges results from {@link ContactTileAdapter} and
* {@link PhoneNumberListAdapter} into one unified list using {@link PhoneFavoriteMergedAdapter}.
* A contact filter header is also inserted between those adapters' results.
*/
public class PhoneFavoriteFragment extends Fragment implements OnItemClickListener {
private static final String TAG = PhoneFavoriteFragment.class.getSimpleName();
private static final boolean DEBUG = false;
/**
* Used with LoaderManager.
*/
private static int LOADER_ID_CONTACT_TILE = 1;
private static int LOADER_ID_ALL_CONTACTS = 2;
private static final String KEY_FILTER = "filter";
private static final int REQUEST_CODE_ACCOUNT_FILTER = 1;
public interface Listener {
public void onContactSelected(Uri contactUri);
}
private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
@Override
public CursorLoader onCreateLoader(int id, Bundle args) {
if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader.");
return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoadFinished");
mContactTileAdapter.setContactCursor(data);
if (mAllContactsForceReload) {
mAllContactsAdapter.onDataReload();
// Use restartLoader() to make LoaderManager to load the section again.
getLoaderManager().restartLoader(
LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
} else if (!mAllContactsLoaderStarted) {
// Load "all" contacts if not loaded yet.
getLoaderManager().initLoader(
LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
}
mAllContactsForceReload = false;
mAllContactsLoaderStarted = true;
// Show the filter header with "loading" state.
updateFilterHeaderView();
mAccountFilterHeader.setVisibility(View.VISIBLE);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. ");
}
}
private class AllContactsLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onCreateLoader");
CursorLoader loader = new CursorLoader(getActivity(), null, null, null, null, null);
mAllContactsAdapter.configureLoader(loader, Directory.DEFAULT);
return loader;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoadFinished");
mAllContactsAdapter.changeCursor(0, data);
updateFilterHeaderView();
mAccountFilterHeaderContainer.setVisibility(View.VISIBLE);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoaderReset. ");
}
}
private class ContactTileAdapterListener implements ContactTileAdapter.Listener {
@Override
public void onContactSelected(Uri contactUri, Rect targetRect) {
if (mListener != null) {
mListener.onContactSelected(contactUri);
}
}
}
private class FilterHeaderClickListener implements OnClickListener {
@Override
public void onClick(View view) {
AccountFilterUtil.startAccountFilterActivityForResult(
PhoneFavoriteFragment.this, REQUEST_CODE_ACCOUNT_FILTER);
}
}
private class ContactsPreferenceChangeListener
implements ContactsPreferences.ChangeListener {
@Override
public void onChange() {
if (loadContactsPreferences()) {
requestReloadAllContacts();
}
}
}
private class ScrollListener implements ListView.OnScrollListener {
private boolean mShouldShowFastScroller;
@Override
public void onScroll(AbsListView view,
int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// FastScroller should be visible only when the user is seeing "all" contacts section.
final boolean shouldShow = mAdapter.shouldShowFirstScroller(firstVisibleItem);
if (shouldShow != mShouldShowFastScroller) {
mListView.setVerticalScrollBarEnabled(shouldShow);
mListView.setFastScrollEnabled(shouldShow);
mListView.setFastScrollAlwaysVisible(shouldShow);
mShouldShowFastScroller = shouldShow;
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
}
private Listener mListener;
private PhoneFavoriteMergedAdapter mAdapter;
private ContactTileAdapter mContactTileAdapter;
private PhoneNumberListAdapter mAllContactsAdapter;
/**
* true when the loader for {@link PhoneNumberListAdapter} has started already.
*/
private boolean mAllContactsLoaderStarted;
/**
* true when the loader for {@link PhoneNumberListAdapter} must reload "all" contacts again.
* It typically happens when {@link ContactsPreferences} has changed its settings
* (display order and sort order)
*/
private boolean mAllContactsForceReload;
private ContactsPreferences mContactsPrefs;
private ContactListFilter mFilter;
private TextView mEmptyView;
private ListView mListView;
/**
* Layout containing {@link #mAccountFilterHeader}. Used to limit area being "pressed".
*/
private FrameLayout mAccountFilterHeaderContainer;
private View mAccountFilterHeader;
private final ContactTileAdapter.Listener mContactTileAdapterListener =
new ContactTileAdapterListener();
private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener =
new ContactTileLoaderListener();
private final LoaderManager.LoaderCallbacks<Cursor> mAllContactsLoaderListener =
new AllContactsLoaderListener();
private final OnClickListener mFilterHeaderClickListener = new FilterHeaderClickListener();
private final ContactsPreferenceChangeListener mContactsPreferenceChangeListener =
new ContactsPreferenceChangeListener();
private final ScrollListener mScrollListener = new ScrollListener();
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
if (savedState != null) {
mFilter = savedState.getParcelable(KEY_FILTER);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_FILTER, mFilter);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mContactsPrefs = new ContactsPreferences(activity);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View listLayout = inflater.inflate(
R.layout.phone_contact_tile_list, container, false);
mListView = (ListView) listLayout.findViewById(R.id.contact_tile_list);
mListView.setItemsCanFocus(true);
mListView.setOnItemClickListener(this);
mListView.setVerticalScrollBarEnabled(false);
mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT);
mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
initAdapters(getActivity(), inflater);
mListView.setAdapter(mAdapter);
mListView.setOnScrollListener(mScrollListener);
mListView.setFastScrollEnabled(false);
mListView.setFastScrollAlwaysVisible(false);
mEmptyView = (TextView) listLayout.findViewById(R.id.contact_tile_list_empty);
mEmptyView.setText(getString(R.string.listTotalAllContactsZero));
mListView.setEmptyView(mEmptyView);
updateFilterHeaderView();
return listLayout;
}
/**
* Constructs and initializes {@link #mContactTileAdapter}, {@link #mAllContactsAdapter}, and
* {@link #mAllContactsAdapter}.
*
* TODO: Move all the code here to {@link PhoneFavoriteMergedAdapter} if possible.
* There are two problems: account header (whose content changes depending on filter settings)
* and OnClickListener (which initiates {@link Activity#startActivityForResult(Intent, int)}).
* See also issue 5429203, 5269692, and 5432286. If we are able to have a singleton for filter,
* this work will become easier.
*/
private void initAdapters(Context context, LayoutInflater inflater) {
mContactTileAdapter = new ContactTileAdapter(context, mContactTileAdapterListener,
getResources().getInteger(R.integer.contact_tile_column_count),
ContactTileAdapter.DisplayType.STREQUENT_PHONE_ONLY);
mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(context));
// Setup the "all" adapter manually. See also the setup logic in ContactEntryListFragment.
mAllContactsAdapter = new PhoneNumberListAdapter(context);
mAllContactsAdapter.setDisplayPhotos(true);
mAllContactsAdapter.setQuickContactEnabled(true);
mAllContactsAdapter.setSearchMode(false);
mAllContactsAdapter.setIncludeProfile(false);
mAllContactsAdapter.setSelectionVisible(false);
mAllContactsAdapter.setDarkTheme(true);
mAllContactsAdapter.setPhotoLoader(ContactPhotoManager.getInstance(context));
// Disable directory header.
mAllContactsAdapter.setHasHeader(0, false);
// Show A-Z section index.
mAllContactsAdapter.setSectionHeaderDisplayEnabled(true);
// Disable pinned header. It doesn't work with this fragment.
mAllContactsAdapter.setPinnedPartitionHeadersEnabled(false);
// Put photos on left for consistency with "frequent" contacts section.
mAllContactsAdapter.setPhotoPosition(ContactListItemView.PhotoPosition.LEFT);
if (mFilter != null) {
mAllContactsAdapter.setFilter(mFilter);
}
// Create the account filter header but keep it hidden until "all" contacts are loaded.
mAccountFilterHeaderContainer = new FrameLayout(context, null);
mAccountFilterHeader = inflater.inflate(R.layout.account_filter_header_for_phone_favorite,
mListView, false);
mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener);
mAccountFilterHeaderContainer.addView(mAccountFilterHeader);
mAccountFilterHeaderContainer.setVisibility(View.GONE);
mAdapter = new PhoneFavoriteMergedAdapter(context,
mContactTileAdapter, mAccountFilterHeaderContainer, mAllContactsAdapter);
}
@Override
public void onStart() {
super.onStart();
mContactsPrefs.registerChangeListener(mContactsPreferenceChangeListener);
// If ContactsPreferences has changed, we need to reload "all" contacts with the new
// settings. If mAllContactsFoarceReload is already true, it should be kept.
if (loadContactsPreferences()) {
mAllContactsForceReload = true;
}
// Use initLoader() instead of reloadLoader() to refraing unnecessary reload.
// This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
// be called, on which we'll check if "all" contacts should be reloaded again or not.
getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
}
@Override
public void onStop() {
super.onStop();
mContactsPrefs.unregisterChangeListener();
}
/**
* {@inheritDoc}
*
* This is only effective for elements provided by {@link #mContactTileAdapter}.
* {@link #mContactTileAdapter} has its own logic for click events.
*/
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final int contactTileAdapterCount = mContactTileAdapter.getCount();
if (position <= contactTileAdapterCount) {
Log.e(TAG, "onItemClick() event for unexpected position. "
+ "The position " + position + " is before \"all\" section. Ignored.");
} else {
final int localPosition = position - mContactTileAdapter.getCount() - 1;
if (mListener != null) {
mListener.onContactSelected(mAllContactsAdapter.getDataUri(localPosition));
}
}
}
@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()");
}
}
}
private boolean loadContactsPreferences() {
if (mContactsPrefs == null || mAllContactsAdapter == null) {
return false;
}
boolean changed = false;
if (mAllContactsAdapter.getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) {
mAllContactsAdapter.setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
changed = true;
}
if (mAllContactsAdapter.getSortOrder() != mContactsPrefs.getSortOrder()) {
mAllContactsAdapter.setSortOrder(mContactsPrefs.getSortOrder());
changed = true;
}
return changed;
}
/**
* Requests to reload "all" contacts. If the section is already loaded, this method will
* force reloading it now. If the section isn't loaded yet, the actual load may be done later
* (on {@link #onStart()}.
*/
private void requestReloadAllContacts() {
if (DEBUG) {
Log.d(TAG, "requestReloadAllContacts()"
+ " mAllContactsAdapter: " + mAllContactsAdapter
+ ", mAllContactsLoaderStarted: " + mAllContactsLoaderStarted);
}
if (mAllContactsAdapter == null || !mAllContactsLoaderStarted) {
// Remember this request until next load on onStart().
mAllContactsForceReload = true;
return;
}
if (DEBUG) Log.d(TAG, "Reload \"all\" contacts now.");
mAllContactsAdapter.onDataReload();
// Use restartLoader() to make LoaderManager to load the section again.
getLoaderManager().restartLoader(LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
}
private void updateFilterHeaderView() {
final ContactListFilter filter = getFilter();
if (mAccountFilterHeader == null || mAllContactsAdapter == null || filter == null) {
return;
}
AccountFilterUtil.updateAccountFilterTitleForPhone(
mAccountFilterHeader, filter, mAllContactsAdapter.isLoading(), true);
}
public ContactListFilter getFilter() {
return mFilter;
}
public void setFilter(ContactListFilter filter) {
if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) {
return;
}
if (DEBUG) {
Log.d(TAG, "setFilter(). old filter (" + mFilter
+ ") will be replaced with new filter (" + filter + ")");
}
mFilter = filter;
if (mAllContactsAdapter != null) {
mAllContactsAdapter.setFilter(mFilter);
requestReloadAllContacts();
updateFilterHeaderView();
}
}
public void setListener(Listener listener) {
mListener = listener;
}
}