blob: 97481bc35a6171f65ae0e06baab25a4dd908fa91 [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.dialer.list;
import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager;
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.os.Handler;
import android.os.Message;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Directory;
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
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;
import com.android.contacts.common.ContactPhotoManager;
import com.android.contacts.common.ContactTileLoaderFactory;
import com.android.contacts.common.dialog.ClearFrequentsDialog;
import com.android.contacts.common.list.ContactListFilter;
import com.android.contacts.common.list.ContactListFilterController;
import com.android.contacts.common.list.ContactListItemView;
import com.android.contacts.common.list.ContactTileAdapter;
import com.android.contacts.common.list.ContactTileView;
import com.android.contacts.common.list.PhoneNumberListAdapter;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.AccountFilterUtil;
import com.android.contacts.common.interactions.ImportExportDialogFragment;
import com.android.dialer.DialtactsActivity;
import com.android.dialer.R;
/**
* 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 com.android.contacts.common.list.ContactTileAdapter} and
* {@link com.android.contacts.common.list.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);
public void onCallNumberDirectly(String phoneNumber);
}
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);
// invalidate the options menu if needed
invalidateOptionsMenuIfNeeded();
}
@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();
mHandler.removeMessages(MESSAGE_SHOW_LOADING_EFFECT);
mLoadingView.setVisibility(View.VISIBLE);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoaderReset. ");
}
}
private class ContactTileAdapterListener implements ContactTileView.Listener {
@Override
public void onContactSelected(Uri contactUri, Rect targetRect) {
if (mListener != null) {
mListener.onContactSelected(contactUri);
}
}
@Override
public void onCallNumberDirectly(String phoneNumber) {
if (mListener != null) {
mListener.onCallNumberDirectly(phoneNumber);
}
}
@Override
public int getApproximateTileWidth() {
return getView().getWidth() / mContactTileAdapter.getColumnCount();
}
}
private class FilterHeaderClickListener implements OnClickListener {
@Override
public void onClick(View view) {
AccountFilterUtil.startAccountFilterActivityForResult(
PhoneFavoriteFragment.this,
REQUEST_CODE_ACCOUNT_FILTER,
mFilter);
}
}
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 static final int MESSAGE_SHOW_LOADING_EFFECT = 1;
private static final int LOADING_EFFECT_DELAY = 500; // ms
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SHOW_LOADING_EFFECT:
mLoadingView.setVisibility(View.VISIBLE);
break;
}
}
};
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;
/**
* Layout used when contacts load is slower than expected and thus "loading" view should be
* shown.
*/
private View mLoadingView;
private final ContactTileView.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();
private boolean mOptionsMenuHasFrequents;
@Override
public void onAttach(Activity activity) {
if (DEBUG) Log.d(TAG, "onAttach()");
super.onAttach(activity);
mContactsPrefs = new ContactsPreferences(activity);
// Construct two base adapters which will become part of PhoneFavoriteMergedAdapter.
// We don't construct the resultant adapter at this moment since it requires LayoutInflater
// that will be available on onCreateView().
mContactTileAdapter = new ContactTileAdapter(activity, mContactTileAdapterListener,
getResources().getInteger(R.integer.contact_tile_column_count_in_favorites),
ContactTileAdapter.DisplayType.STREQUENT_PHONE_ONLY);
mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity));
// Setup the "all" adapter manually. See also the setup logic in ContactEntryListFragment.
mAllContactsAdapter = new PhoneNumberListAdapter(activity);
mAllContactsAdapter.setDisplayPhotos(true);
mAllContactsAdapter.setQuickContactEnabled(true);
mAllContactsAdapter.setSearchMode(false);
mAllContactsAdapter.setIncludeProfile(false);
mAllContactsAdapter.setSelectionVisible(false);
mAllContactsAdapter.setDarkTheme(true);
mAllContactsAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity));
// 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 START (LEFT in LTR layout direction and RIGHT in RTL layout direction)
// for consistency with "frequent" contacts section.
mAllContactsAdapter.setPhotoPosition(ContactListItemView.getDefaultPhotoPosition(
true /* opposite */ ));
// Use Callable.CONTENT_URI which will include not only phone numbers but also SIP
// addresses.
mAllContactsAdapter.setUseCallableUri(true);
mAllContactsAdapter.setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
mAllContactsAdapter.setSortOrder(mContactsPrefs.getSortOrder());
}
@Override
public void onCreate(Bundle savedState) {
if (DEBUG) Log.d(TAG, "onCreate()");
super.onCreate(savedState);
if (savedState != null) {
mFilter = savedState.getParcelable(KEY_FILTER);
if (mFilter != null) {
mAllContactsAdapter.setFilter(mFilter);
}
}
setHasOptionsMenu(true);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_FILTER, mFilter);
}
@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);
// Create the account filter header but keep it hidden until "all" contacts are loaded.
mAccountFilterHeaderContainer = new FrameLayout(getActivity(), null);
mAccountFilterHeader = inflater.inflate(R.layout.account_filter_header_for_phone_favorite,
mListView, false);
mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener);
mAccountFilterHeaderContainer.addView(mAccountFilterHeader);
mLoadingView = inflater.inflate(R.layout.phone_loading_contacts, mListView, false);
mAdapter = new PhoneFavoriteMergedAdapter(getActivity(),
mContactTileAdapter, mAccountFilterHeaderContainer, mAllContactsAdapter,
mLoadingView);
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;
}
private boolean isOptionsMenuChanged() {
return mOptionsMenuHasFrequents != hasFrequents();
}
private void invalidateOptionsMenuIfNeeded() {
if (isOptionsMenuChanged()) {
getActivity().invalidateOptionsMenu();
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.phone_favorite_options, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents);
mOptionsMenuHasFrequents = hasFrequents();
clearFrequents.setVisible(mOptionsMenuHasFrequents);
}
private boolean hasFrequents() {
return mContactTileAdapter.getNumFrequents() > 0;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_import_export:
// We hard-code the "contactsAreAvailable" argument because doing it properly would
// involve querying a {@link ProviderStatusLoader}, which we don't want to do right
// now in Dialtacts for (potential) performance reasons. Compare with how it is
// done in {@link PeopleActivity}.
ImportExportDialogFragment.show(getFragmentManager(), true,
DialtactsActivity.class);
return true;
case R.id.menu_accounts:
final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] {
ContactsContract.AUTHORITY
});
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
startActivity(intent);
return true;
case R.id.menu_clear_frequents:
ClearFrequentsDialog.show(getFragmentManager());
return true;
}
return false;
}
@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 restartLoader() to refraining 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);
// Delay showing "loading" view until certain amount of time so that users won't see
// instant flash of the view when the contacts load is fast enough.
// This will be kept shown until both tile and all sections are loaded.
mLoadingView.setVisibility(View.INVISIBLE);
mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_LOADING_EFFECT, LOADING_EFFECT_DELAY);
}
@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;
final int currentDisplayOrder = mContactsPrefs.getDisplayOrder();
if (mAllContactsAdapter.getContactNameDisplayOrder() != currentDisplayOrder) {
mAllContactsAdapter.setContactNameDisplayOrder(currentDisplayOrder);
changed = true;
}
final int currentSortOrder = mContactsPrefs.getSortOrder();
if (mAllContactsAdapter.getSortOrder() != currentSortOrder) {
mAllContactsAdapter.setSortOrder(currentSortOrder);
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, 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;
}
}