Home | History | Annotate | Download | only in list
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.contacts.list;
     17 
     18 import com.android.contacts.ContactPhotoManager;
     19 import com.android.contacts.ContactTileLoaderFactory;
     20 import com.android.contacts.R;
     21 import com.android.contacts.dialog.ClearFrequentsDialog;
     22 import com.android.contacts.interactions.ImportExportDialogFragment;
     23 import com.android.contacts.preference.ContactsPreferences;
     24 import com.android.contacts.util.AccountFilterUtil;
     25 
     26 import android.app.Activity;
     27 import android.app.Fragment;
     28 import android.app.LoaderManager;
     29 import android.content.CursorLoader;
     30 import android.content.Intent;
     31 import android.content.Loader;
     32 import android.database.Cursor;
     33 import android.graphics.Rect;
     34 import android.net.Uri;
     35 import android.os.Bundle;
     36 import android.os.Handler;
     37 import android.os.Message;
     38 import android.provider.ContactsContract;
     39 import android.provider.ContactsContract.Directory;
     40 import android.provider.Settings;
     41 import android.util.Log;
     42 import android.view.LayoutInflater;
     43 import android.view.Menu;
     44 import android.view.MenuInflater;
     45 import android.view.MenuItem;
     46 import android.view.View;
     47 import android.view.View.OnClickListener;
     48 import android.view.ViewGroup;
     49 import android.widget.AbsListView;
     50 import android.widget.AdapterView;
     51 import android.widget.AdapterView.OnItemClickListener;
     52 import android.widget.FrameLayout;
     53 import android.widget.ListView;
     54 import android.widget.TextView;
     55 
     56 /**
     57  * Fragment for Phone UI's favorite screen.
     58  *
     59  * This fragment contains three kinds of contacts in one screen: "starred", "frequent", and "all"
     60  * contacts. To show them at once, this merges results from {@link ContactTileAdapter} and
     61  * {@link PhoneNumberListAdapter} into one unified list using {@link PhoneFavoriteMergedAdapter}.
     62  * A contact filter header is also inserted between those adapters' results.
     63  */
     64 public class PhoneFavoriteFragment extends Fragment implements OnItemClickListener {
     65     private static final String TAG = PhoneFavoriteFragment.class.getSimpleName();
     66     private static final boolean DEBUG = false;
     67 
     68     /**
     69      * Used with LoaderManager.
     70      */
     71     private static int LOADER_ID_CONTACT_TILE = 1;
     72     private static int LOADER_ID_ALL_CONTACTS = 2;
     73 
     74     private static final String KEY_FILTER = "filter";
     75 
     76     private static final int REQUEST_CODE_ACCOUNT_FILTER = 1;
     77 
     78     public interface Listener {
     79         public void onContactSelected(Uri contactUri);
     80         public void onCallNumberDirectly(String phoneNumber);
     81     }
     82 
     83     private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
     84         @Override
     85         public CursorLoader onCreateLoader(int id, Bundle args) {
     86             if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader.");
     87             return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
     88         }
     89 
     90         @Override
     91         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
     92             if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoadFinished");
     93             mContactTileAdapter.setContactCursor(data);
     94 
     95             if (mAllContactsForceReload) {
     96                 mAllContactsAdapter.onDataReload();
     97                 // Use restartLoader() to make LoaderManager to load the section again.
     98                 getLoaderManager().restartLoader(
     99                         LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
    100             } else if (!mAllContactsLoaderStarted) {
    101                 // Load "all" contacts if not loaded yet.
    102                 getLoaderManager().initLoader(
    103                         LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
    104             }
    105             mAllContactsForceReload = false;
    106             mAllContactsLoaderStarted = true;
    107 
    108             // Show the filter header with "loading" state.
    109             updateFilterHeaderView();
    110             mAccountFilterHeader.setVisibility(View.VISIBLE);
    111 
    112             // invalidate the options menu if needed
    113             invalidateOptionsMenuIfNeeded();
    114         }
    115 
    116         @Override
    117         public void onLoaderReset(Loader<Cursor> loader) {
    118             if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. ");
    119         }
    120     }
    121 
    122     private class AllContactsLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
    123         @Override
    124         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    125             if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onCreateLoader");
    126             CursorLoader loader = new CursorLoader(getActivity(), null, null, null, null, null);
    127             mAllContactsAdapter.configureLoader(loader, Directory.DEFAULT);
    128             return loader;
    129         }
    130 
    131         @Override
    132         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    133             if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoadFinished");
    134             mAllContactsAdapter.changeCursor(0, data);
    135             updateFilterHeaderView();
    136             mHandler.removeMessages(MESSAGE_SHOW_LOADING_EFFECT);
    137             mLoadingView.setVisibility(View.VISIBLE);
    138         }
    139 
    140         @Override
    141         public void onLoaderReset(Loader<Cursor> loader) {
    142             if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoaderReset. ");
    143         }
    144     }
    145 
    146     private class ContactTileAdapterListener implements ContactTileView.Listener {
    147         @Override
    148         public void onContactSelected(Uri contactUri, Rect targetRect) {
    149             if (mListener != null) {
    150                 mListener.onContactSelected(contactUri);
    151             }
    152         }
    153 
    154         @Override
    155         public void onCallNumberDirectly(String phoneNumber) {
    156             if (mListener != null) {
    157                 mListener.onCallNumberDirectly(phoneNumber);
    158             }
    159         }
    160 
    161         @Override
    162         public int getApproximateTileWidth() {
    163             return getView().getWidth() / mContactTileAdapter.getColumnCount();
    164         }
    165     }
    166 
    167     private class FilterHeaderClickListener implements OnClickListener {
    168         @Override
    169         public void onClick(View view) {
    170             AccountFilterUtil.startAccountFilterActivityForResult(
    171                     PhoneFavoriteFragment.this,
    172                     REQUEST_CODE_ACCOUNT_FILTER,
    173                     mFilter);
    174         }
    175     }
    176 
    177     private class ContactsPreferenceChangeListener
    178             implements ContactsPreferences.ChangeListener {
    179         @Override
    180         public void onChange() {
    181             if (loadContactsPreferences()) {
    182                 requestReloadAllContacts();
    183             }
    184         }
    185     }
    186 
    187     private class ScrollListener implements ListView.OnScrollListener {
    188         private boolean mShouldShowFastScroller;
    189         @Override
    190         public void onScroll(AbsListView view,
    191                 int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    192             // FastScroller should be visible only when the user is seeing "all" contacts section.
    193             final boolean shouldShow = mAdapter.shouldShowFirstScroller(firstVisibleItem);
    194             if (shouldShow != mShouldShowFastScroller) {
    195                 mListView.setVerticalScrollBarEnabled(shouldShow);
    196                 mListView.setFastScrollEnabled(shouldShow);
    197                 mListView.setFastScrollAlwaysVisible(shouldShow);
    198                 mShouldShowFastScroller = shouldShow;
    199             }
    200         }
    201 
    202         @Override
    203         public void onScrollStateChanged(AbsListView view, int scrollState) {
    204         }
    205     }
    206 
    207     private static final int MESSAGE_SHOW_LOADING_EFFECT = 1;
    208     private static final int LOADING_EFFECT_DELAY = 500;  // ms
    209     private final Handler mHandler = new Handler() {
    210         @Override
    211         public void handleMessage(Message msg) {
    212             switch (msg.what) {
    213                 case MESSAGE_SHOW_LOADING_EFFECT:
    214                     mLoadingView.setVisibility(View.VISIBLE);
    215                     break;
    216             }
    217         }
    218     };
    219 
    220     private Listener mListener;
    221     private PhoneFavoriteMergedAdapter mAdapter;
    222     private ContactTileAdapter mContactTileAdapter;
    223     private PhoneNumberListAdapter mAllContactsAdapter;
    224 
    225     /**
    226      * true when the loader for {@link PhoneNumberListAdapter} has started already.
    227      */
    228     private boolean mAllContactsLoaderStarted;
    229     /**
    230      * true when the loader for {@link PhoneNumberListAdapter} must reload "all" contacts again.
    231      * It typically happens when {@link ContactsPreferences} has changed its settings
    232      * (display order and sort order)
    233      */
    234     private boolean mAllContactsForceReload;
    235 
    236     private ContactsPreferences mContactsPrefs;
    237     private ContactListFilter mFilter;
    238 
    239     private TextView mEmptyView;
    240     private ListView mListView;
    241     /**
    242      * Layout containing {@link #mAccountFilterHeader}. Used to limit area being "pressed".
    243      */
    244     private FrameLayout mAccountFilterHeaderContainer;
    245     private View mAccountFilterHeader;
    246 
    247     /**
    248      * Layout used when contacts load is slower than expected and thus "loading" view should be
    249      * shown.
    250      */
    251     private View mLoadingView;
    252 
    253     private final ContactTileView.Listener mContactTileAdapterListener =
    254             new ContactTileAdapterListener();
    255     private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener =
    256             new ContactTileLoaderListener();
    257     private final LoaderManager.LoaderCallbacks<Cursor> mAllContactsLoaderListener =
    258             new AllContactsLoaderListener();
    259     private final OnClickListener mFilterHeaderClickListener = new FilterHeaderClickListener();
    260     private final ContactsPreferenceChangeListener mContactsPreferenceChangeListener =
    261             new ContactsPreferenceChangeListener();
    262     private final ScrollListener mScrollListener = new ScrollListener();
    263 
    264     private boolean mOptionsMenuHasFrequents;
    265 
    266     @Override
    267     public void onAttach(Activity activity) {
    268         if (DEBUG) Log.d(TAG, "onAttach()");
    269         super.onAttach(activity);
    270 
    271         mContactsPrefs = new ContactsPreferences(activity);
    272 
    273         // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter.
    274         // We don't construct the resultant adapter at this moment since it requires LayoutInflater
    275         // that will be available on onCreateView().
    276 
    277         mContactTileAdapter = new ContactTileAdapter(activity, mContactTileAdapterListener,
    278                 getResources().getInteger(R.integer.contact_tile_column_count_in_favorites),
    279                 ContactTileAdapter.DisplayType.STREQUENT_PHONE_ONLY);
    280         mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity));
    281 
    282         // Setup the "all" adapter manually. See also the setup logic in ContactEntryListFragment.
    283         mAllContactsAdapter = new PhoneNumberListAdapter(activity);
    284         mAllContactsAdapter.setDisplayPhotos(true);
    285         mAllContactsAdapter.setQuickContactEnabled(true);
    286         mAllContactsAdapter.setSearchMode(false);
    287         mAllContactsAdapter.setIncludeProfile(false);
    288         mAllContactsAdapter.setSelectionVisible(false);
    289         mAllContactsAdapter.setDarkTheme(true);
    290         mAllContactsAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity));
    291         // Disable directory header.
    292         mAllContactsAdapter.setHasHeader(0, false);
    293         // Show A-Z section index.
    294         mAllContactsAdapter.setSectionHeaderDisplayEnabled(true);
    295         // Disable pinned header. It doesn't work with this fragment.
    296         mAllContactsAdapter.setPinnedPartitionHeadersEnabled(false);
    297         // Put photos on left for consistency with "frequent" contacts section.
    298         mAllContactsAdapter.setPhotoPosition(ContactListItemView.PhotoPosition.LEFT);
    299 
    300         // Use Callable.CONTENT_URI which will include not only phone numbers but also SIP
    301         // addresses.
    302         mAllContactsAdapter.setUseCallableUri(true);
    303 
    304         mAllContactsAdapter.setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
    305         mAllContactsAdapter.setSortOrder(mContactsPrefs.getSortOrder());
    306     }
    307 
    308     @Override
    309     public void onCreate(Bundle savedState) {
    310         if (DEBUG) Log.d(TAG, "onCreate()");
    311         super.onCreate(savedState);
    312         if (savedState != null) {
    313             mFilter = savedState.getParcelable(KEY_FILTER);
    314 
    315             if (mFilter != null) {
    316                 mAllContactsAdapter.setFilter(mFilter);
    317             }
    318         }
    319         setHasOptionsMenu(true);
    320     }
    321 
    322     @Override
    323     public void onSaveInstanceState(Bundle outState) {
    324         super.onSaveInstanceState(outState);
    325         outState.putParcelable(KEY_FILTER, mFilter);
    326     }
    327 
    328     @Override
    329     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    330             Bundle savedInstanceState) {
    331         final View listLayout = inflater.inflate(
    332                 R.layout.phone_contact_tile_list, container, false);
    333 
    334         mListView = (ListView) listLayout.findViewById(R.id.contact_tile_list);
    335         mListView.setItemsCanFocus(true);
    336         mListView.setOnItemClickListener(this);
    337         mListView.setVerticalScrollBarEnabled(false);
    338         mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT);
    339         mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
    340 
    341         // Create the account filter header but keep it hidden until "all" contacts are loaded.
    342         mAccountFilterHeaderContainer = new FrameLayout(getActivity(), null);
    343         mAccountFilterHeader = inflater.inflate(R.layout.account_filter_header_for_phone_favorite,
    344                 mListView, false);
    345         mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener);
    346         mAccountFilterHeaderContainer.addView(mAccountFilterHeader);
    347 
    348         mLoadingView = inflater.inflate(R.layout.phone_loading_contacts, mListView, false);
    349 
    350         mAdapter = new PhoneFavoriteMergedAdapter(getActivity(),
    351                 mContactTileAdapter, mAccountFilterHeaderContainer, mAllContactsAdapter,
    352                 mLoadingView);
    353 
    354         mListView.setAdapter(mAdapter);
    355 
    356         mListView.setOnScrollListener(mScrollListener);
    357         mListView.setFastScrollEnabled(false);
    358         mListView.setFastScrollAlwaysVisible(false);
    359 
    360         mEmptyView = (TextView) listLayout.findViewById(R.id.contact_tile_list_empty);
    361         mEmptyView.setText(getString(R.string.listTotalAllContactsZero));
    362         mListView.setEmptyView(mEmptyView);
    363 
    364         updateFilterHeaderView();
    365 
    366         return listLayout;
    367     }
    368 
    369     private boolean isOptionsMenuChanged() {
    370         return mOptionsMenuHasFrequents != hasFrequents();
    371     }
    372 
    373     private void invalidateOptionsMenuIfNeeded() {
    374         if (isOptionsMenuChanged()) {
    375             getActivity().invalidateOptionsMenu();
    376         }
    377     }
    378 
    379     @Override
    380     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    381         super.onCreateOptionsMenu(menu, inflater);
    382         inflater.inflate(R.menu.phone_favorite_options, menu);
    383     }
    384 
    385     @Override
    386     public void onPrepareOptionsMenu(Menu menu) {
    387         final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents);
    388         mOptionsMenuHasFrequents = hasFrequents();
    389         clearFrequents.setVisible(mOptionsMenuHasFrequents);
    390     }
    391 
    392     private boolean hasFrequents() {
    393         return mContactTileAdapter.getNumFrequents() > 0;
    394     }
    395 
    396     @Override
    397     public boolean onOptionsItemSelected(MenuItem item) {
    398         switch (item.getItemId()) {
    399             case R.id.menu_import_export:
    400                 // We hard-code the "contactsAreAvailable" argument because doing it properly would
    401                 // involve querying a {@link ProviderStatusLoader}, which we don't want to do right
    402                 // now in Dialtacts for (potential) performance reasons.  Compare with how it is
    403                 // done in {@link PeopleActivity}.
    404                 ImportExportDialogFragment.show(getFragmentManager(), true);
    405                 return true;
    406             case R.id.menu_accounts:
    407                 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
    408                 intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] {
    409                     ContactsContract.AUTHORITY
    410                 });
    411                 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    412                 startActivity(intent);
    413                 return true;
    414             case R.id.menu_clear_frequents:
    415                 ClearFrequentsDialog.show(getFragmentManager());
    416                 return true;
    417         }
    418         return false;
    419     }
    420 
    421     @Override
    422     public void onStart() {
    423         super.onStart();
    424 
    425         mContactsPrefs.registerChangeListener(mContactsPreferenceChangeListener);
    426 
    427         // If ContactsPreferences has changed, we need to reload "all" contacts with the new
    428         // settings. If mAllContactsFoarceReload is already true, it should be kept.
    429         if (loadContactsPreferences()) {
    430             mAllContactsForceReload = true;
    431         }
    432 
    433         // Use initLoader() instead of restartLoader() to refraining unnecessary reload.
    434         // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
    435         // be called, on which we'll check if "all" contacts should be reloaded again or not.
    436         getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
    437 
    438         // Delay showing "loading" view until certain amount of time so that users won't see
    439         // instant flash of the view when the contacts load is fast enough.
    440         // This will be kept shown until both tile and all sections are loaded.
    441         mLoadingView.setVisibility(View.INVISIBLE);
    442         mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_LOADING_EFFECT, LOADING_EFFECT_DELAY);
    443     }
    444 
    445     @Override
    446     public void onStop() {
    447         super.onStop();
    448         mContactsPrefs.unregisterChangeListener();
    449     }
    450 
    451     /**
    452      * {@inheritDoc}
    453      *
    454      * This is only effective for elements provided by {@link #mContactTileAdapter}.
    455      * {@link #mContactTileAdapter} has its own logic for click events.
    456      */
    457     @Override
    458     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    459         final int contactTileAdapterCount = mContactTileAdapter.getCount();
    460         if (position <= contactTileAdapterCount) {
    461             Log.e(TAG, "onItemClick() event for unexpected position. "
    462                     + "The position " + position + " is before \"all\" section. Ignored.");
    463         } else {
    464             final int localPosition = position - mContactTileAdapter.getCount() - 1;
    465             if (mListener != null) {
    466                 mListener.onContactSelected(mAllContactsAdapter.getDataUri(localPosition));
    467             }
    468         }
    469     }
    470 
    471     @Override
    472     public void onActivityResult(int requestCode, int resultCode, Intent data) {
    473         if (requestCode == REQUEST_CODE_ACCOUNT_FILTER) {
    474             if (getActivity() != null) {
    475                 AccountFilterUtil.handleAccountFilterResult(
    476                         ContactListFilterController.getInstance(getActivity()), resultCode, data);
    477             } else {
    478                 Log.e(TAG, "getActivity() returns null during Fragment#onActivityResult()");
    479             }
    480         }
    481     }
    482 
    483     private boolean loadContactsPreferences() {
    484         if (mContactsPrefs == null || mAllContactsAdapter == null) {
    485             return false;
    486         }
    487 
    488         boolean changed = false;
    489         final int currentDisplayOrder = mContactsPrefs.getDisplayOrder();
    490         if (mAllContactsAdapter.getContactNameDisplayOrder() != currentDisplayOrder) {
    491             mAllContactsAdapter.setContactNameDisplayOrder(currentDisplayOrder);
    492             changed = true;
    493         }
    494 
    495         final int currentSortOrder = mContactsPrefs.getSortOrder();
    496         if (mAllContactsAdapter.getSortOrder() != currentSortOrder) {
    497             mAllContactsAdapter.setSortOrder(currentSortOrder);
    498             changed = true;
    499         }
    500 
    501         return changed;
    502     }
    503 
    504     /**
    505      * Requests to reload "all" contacts. If the section is already loaded, this method will
    506      * force reloading it now. If the section isn't loaded yet, the actual load may be done later
    507      * (on {@link #onStart()}.
    508      */
    509     private void requestReloadAllContacts() {
    510         if (DEBUG) {
    511             Log.d(TAG, "requestReloadAllContacts()"
    512                     + " mAllContactsAdapter: " + mAllContactsAdapter
    513                     + ", mAllContactsLoaderStarted: " + mAllContactsLoaderStarted);
    514         }
    515 
    516         if (mAllContactsAdapter == null || !mAllContactsLoaderStarted) {
    517             // Remember this request until next load on onStart().
    518             mAllContactsForceReload = true;
    519             return;
    520         }
    521 
    522         if (DEBUG) Log.d(TAG, "Reload \"all\" contacts now.");
    523 
    524         mAllContactsAdapter.onDataReload();
    525         // Use restartLoader() to make LoaderManager to load the section again.
    526         getLoaderManager().restartLoader(LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
    527     }
    528 
    529     private void updateFilterHeaderView() {
    530         final ContactListFilter filter = getFilter();
    531         if (mAccountFilterHeader == null || mAllContactsAdapter == null || filter == null) {
    532             return;
    533         }
    534         AccountFilterUtil.updateAccountFilterTitleForPhone(mAccountFilterHeader, filter, true);
    535     }
    536 
    537     public ContactListFilter getFilter() {
    538         return mFilter;
    539     }
    540 
    541     public void setFilter(ContactListFilter filter) {
    542         if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) {
    543             return;
    544         }
    545 
    546         if (DEBUG) {
    547             Log.d(TAG, "setFilter(). old filter (" + mFilter
    548                     + ") will be replaced with new filter (" + filter + ")");
    549         }
    550 
    551         mFilter = filter;
    552 
    553         if (mAllContactsAdapter != null) {
    554             mAllContactsAdapter.setFilter(mFilter);
    555             requestReloadAllContacts();
    556             updateFilterHeaderView();
    557         }
    558     }
    559 
    560     public void setListener(Listener listener) {
    561         mListener = listener;
    562     }
    563 }
    564