/* * 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.ContactPhotoManager; import com.android.contacts.ContactTileLoaderFactory; import com.android.contacts.dialog.ClearFrequentsDialog; import com.android.contacts.interactions.ImportExportDialogFragment; import com.android.contacts.list.ContactListFilter; import com.android.contacts.list.ContactListFilterController; import com.android.contacts.list.ContactListItemView; import com.android.contacts.list.ContactTileAdapter; import com.android.contacts.list.ContactTileView; import com.android.contacts.list.PhoneNumberListAdapter; import com.android.contacts.preference.ContactsPreferences; import com.android.contacts.util.AccountFilterUtil; 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.list.ContactTileAdapter} and * {@link com.android.contacts.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 { @Override public CursorLoader onCreateLoader(int id, Bundle args) { if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader."); return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity()); } @Override public void onLoadFinished(Loader 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 loader) { if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. "); } } private class AllContactsLoaderListener implements LoaderManager.LoaderCallbacks { @Override public Loader 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 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 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 mContactTileLoaderListener = new ContactTileLoaderListener(); private final LoaderManager.LoaderCallbacks 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 left for consistency with "frequent" contacts section. mAllContactsAdapter.setPhotoPosition(ContactListItemView.PhotoPosition.LEFT); // 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); 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; } }