1/*
2 * Copyright (C) 2010 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
17package com.android.contacts.activities;
18
19import android.app.Fragment;
20import android.app.FragmentManager;
21import android.app.FragmentTransaction;
22import android.content.Intent;
23import android.graphics.Rect;
24import android.net.Uri;
25import android.os.Bundle;
26import android.os.Parcelable;
27import android.os.UserManager;
28import android.preference.PreferenceActivity;
29import android.provider.ContactsContract;
30import android.provider.ContactsContract.Contacts;
31import android.provider.ContactsContract.ProviderStatus;
32import android.provider.ContactsContract.QuickContact;
33import android.provider.Settings;
34import android.support.v13.app.FragmentPagerAdapter;
35import android.support.v4.view.PagerAdapter;
36import android.support.v4.view.ViewPager;
37import android.text.TextUtils;
38import android.util.Log;
39import android.view.KeyCharacterMap;
40import android.view.KeyEvent;
41import android.view.Menu;
42import android.view.MenuInflater;
43import android.view.MenuItem;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.Window;
47import android.widget.ImageButton;
48import android.widget.Toolbar;
49
50import com.android.contacts.ContactsActivity;
51import com.android.contacts.R;
52import com.android.contacts.activities.ActionBarAdapter.TabState;
53import com.android.contacts.common.ContactsUtils;
54import com.android.contacts.common.dialog.ClearFrequentsDialog;
55import com.android.contacts.interactions.ContactDeletionInteraction;
56import com.android.contacts.common.interactions.ImportExportDialogFragment;
57import com.android.contacts.common.list.ContactEntryListFragment;
58import com.android.contacts.common.list.ContactListFilter;
59import com.android.contacts.common.list.ContactListFilterController;
60import com.android.contacts.common.list.ContactTileAdapter.DisplayType;
61import com.android.contacts.list.ContactTileListFragment;
62import com.android.contacts.list.ContactsIntentResolver;
63import com.android.contacts.list.ContactsRequest;
64import com.android.contacts.list.ContactsUnavailableFragment;
65import com.android.contacts.list.DefaultContactBrowseListFragment;
66import com.android.contacts.common.list.DirectoryListLoader;
67import com.android.contacts.common.preference.DisplayOptionsPreferenceFragment;
68import com.android.contacts.list.OnContactBrowserActionListener;
69import com.android.contacts.list.OnContactsUnavailableActionListener;
70import com.android.contacts.list.ProviderStatusWatcher;
71import com.android.contacts.list.ProviderStatusWatcher.ProviderStatusListener;
72import com.android.contacts.common.list.ViewPagerTabs;
73import com.android.contacts.preference.ContactsPreferenceActivity;
74import com.android.contacts.common.util.AccountFilterUtil;
75import com.android.contacts.common.util.ViewUtil;
76import com.android.contacts.quickcontact.QuickContactActivity;
77import com.android.contacts.util.AccountPromptUtils;
78import com.android.contacts.common.util.Constants;
79import com.android.contacts.util.DialogManager;
80import com.android.contacts.util.HelpUtils;
81
82import java.util.Locale;
83import java.util.concurrent.atomic.AtomicInteger;
84
85/**
86 * Displays a list to browse contacts.
87 */
88public class PeopleActivity extends ContactsActivity implements
89        View.OnCreateContextMenuListener,
90        View.OnClickListener,
91        ActionBarAdapter.Listener,
92        DialogManager.DialogShowingViewActivity,
93        ContactListFilterController.ContactListFilterListener,
94        ProviderStatusListener {
95
96    private static final String TAG = "PeopleActivity";
97
98    private static final String ENABLE_DEBUG_OPTIONS_HIDDEN_CODE = "debug debug!";
99
100    // These values needs to start at 2. See {@link ContactEntryListFragment}.
101    private static final int SUBACTIVITY_ACCOUNT_FILTER = 2;
102
103    private final DialogManager mDialogManager = new DialogManager(this);
104
105    private ContactsIntentResolver mIntentResolver;
106    private ContactsRequest mRequest;
107
108    private ActionBarAdapter mActionBarAdapter;
109
110    private ContactTileListFragment.Listener mFavoritesFragmentListener =
111            new StrequentContactListFragmentListener();
112
113    private ContactListFilterController mContactListFilterController;
114
115    private ContactsUnavailableFragment mContactsUnavailableFragment;
116    private ProviderStatusWatcher mProviderStatusWatcher;
117    private ProviderStatusWatcher.Status mProviderStatus;
118
119    private boolean mOptionsMenuContactsAvailable;
120
121    /**
122     * Showing a list of Contacts. Also used for showing search results in search mode.
123     */
124    private DefaultContactBrowseListFragment mAllFragment;
125    private ContactTileListFragment mFavoritesFragment;
126
127    /** ViewPager for swipe */
128    private ViewPager mTabPager;
129    private ViewPagerTabs mViewPagerTabs;
130    private TabPagerAdapter mTabPagerAdapter;
131    private String[] mTabTitles;
132    private final TabPagerListener mTabPagerListener = new TabPagerListener();
133
134    private boolean mEnableDebugMenuOptions;
135
136    /**
137     * True if this activity instance is a re-created one.  i.e. set true after orientation change.
138     * This is set in {@link #onCreate} for later use in {@link #onStart}.
139     */
140    private boolean mIsRecreatedInstance;
141
142    /**
143     * If {@link #configureFragments(boolean)} is already called.  Used to avoid calling it twice
144     * in {@link #onStart}.
145     * (This initialization only needs to be done once in onStart() when the Activity was just
146     * created from scratch -- i.e. onCreate() was just called)
147     */
148    private boolean mFragmentInitialized;
149
150    /**
151     * This is to disable {@link #onOptionsItemSelected} when we trying to stop the activity.
152     */
153    private boolean mDisableOptionItemSelected;
154
155    /** Sequential ID assigned to each instance; used for logging */
156    private final int mInstanceId;
157    private static final AtomicInteger sNextInstanceId = new AtomicInteger();
158
159    public PeopleActivity() {
160        mInstanceId = sNextInstanceId.getAndIncrement();
161        mIntentResolver = new ContactsIntentResolver(this);
162        mProviderStatusWatcher = ProviderStatusWatcher.getInstance(this);
163    }
164
165    @Override
166    public String toString() {
167        // Shown on logcat
168        return String.format("%s@%d", getClass().getSimpleName(), mInstanceId);
169    }
170
171    public boolean areContactsAvailable() {
172        return (mProviderStatus != null)
173                && mProviderStatus.status == ProviderStatus.STATUS_NORMAL;
174    }
175
176    private boolean areContactWritableAccountsAvailable() {
177        return ContactsUtils.areContactWritableAccountsAvailable(this);
178    }
179
180    private boolean areGroupWritableAccountsAvailable() {
181        return ContactsUtils.areGroupWritableAccountsAvailable(this);
182    }
183
184    /**
185     * Initialize fragments that are (or may not be) in the layout.
186     *
187     * For the fragments that are in the layout, we initialize them in
188     * {@link #createViewsAndFragments(Bundle)} after inflating the layout.
189     *
190     * However, the {@link ContactsUnavailableFragment} is a special fragment which may not
191     * be in the layout, so we have to do the initialization here.
192     *
193     * The ContactsUnavailableFragment is always created at runtime.
194     */
195    @Override
196    public void onAttachFragment(Fragment fragment) {
197        if (fragment instanceof ContactsUnavailableFragment) {
198            mContactsUnavailableFragment = (ContactsUnavailableFragment)fragment;
199            mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
200                    new ContactsUnavailableFragmentListener());
201        }
202    }
203
204    @Override
205    protected void onCreate(Bundle savedState) {
206        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
207            Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate start");
208        }
209        super.onCreate(savedState);
210
211        if (!processIntent(false)) {
212            finish();
213            return;
214        }
215        mContactListFilterController = ContactListFilterController.getInstance(this);
216        mContactListFilterController.checkFilterValidity(false);
217        mContactListFilterController.addListener(this);
218
219        mProviderStatusWatcher.addListener(this);
220
221        mIsRecreatedInstance = (savedState != null);
222        createViewsAndFragments(savedState);
223
224        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
225            Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate finish");
226        }
227        getWindow().setBackgroundDrawable(null);
228    }
229
230    @Override
231    protected void onNewIntent(Intent intent) {
232        setIntent(intent);
233        if (!processIntent(true)) {
234            finish();
235            return;
236        }
237        mActionBarAdapter.initialize(null, mRequest);
238
239        mContactListFilterController.checkFilterValidity(false);
240
241        // Re-configure fragments.
242        configureFragments(true /* from request */);
243        invalidateOptionsMenuIfNeeded();
244    }
245
246    /**
247     * Resolve the intent and initialize {@link #mRequest}, and launch another activity if redirect
248     * is needed.
249     *
250     * @param forNewIntent set true if it's called from {@link #onNewIntent(Intent)}.
251     * @return {@code true} if {@link PeopleActivity} should continue running.  {@code false}
252     *         if it shouldn't, in which case the caller should finish() itself and shouldn't do
253     *         farther initialization.
254     */
255    private boolean processIntent(boolean forNewIntent) {
256        // Extract relevant information from the intent
257        mRequest = mIntentResolver.resolveIntent(getIntent());
258        if (Log.isLoggable(TAG, Log.DEBUG)) {
259            Log.d(TAG, this + " processIntent: forNewIntent=" + forNewIntent
260                    + " intent=" + getIntent() + " request=" + mRequest);
261        }
262        if (!mRequest.isValid()) {
263            setResult(RESULT_CANCELED);
264            return false;
265        }
266
267        Intent redirect = mRequest.getRedirectIntent();
268        if (redirect != null) {
269            // Need to start a different activity
270            startActivity(redirect);
271            return false;
272        }
273
274        if (mRequest.getActionCode() == ContactsRequest.ACTION_VIEW_CONTACT) {
275            redirect = new Intent(this, QuickContactActivity.class);
276            redirect.setAction(Intent.ACTION_VIEW);
277            redirect.setData(mRequest.getContactUri());
278            startActivity(redirect);
279            return false;
280        }
281        return true;
282    }
283
284    private void createViewsAndFragments(Bundle savedState) {
285        // Disable the ActionBar so that we can use a Toolbar. This needs to be called before
286        // setContentView().
287        getWindow().requestFeature(Window.FEATURE_NO_TITLE);
288
289        setContentView(R.layout.people_activity);
290
291        final FragmentManager fragmentManager = getFragmentManager();
292
293        // Hide all tabs (the current tab will later be reshown once a tab is selected)
294        final FragmentTransaction transaction = fragmentManager.beginTransaction();
295
296        mTabTitles = new String[TabState.COUNT];
297        mTabTitles[TabState.FAVORITES] = getString(R.string.favorites_tab_label);
298        mTabTitles[TabState.ALL] = getString(R.string.all_contacts_tab_label);
299        mTabPager = getView(R.id.tab_pager);
300        mTabPagerAdapter = new TabPagerAdapter();
301        mTabPager.setAdapter(mTabPagerAdapter);
302        mTabPager.setOnPageChangeListener(mTabPagerListener);
303
304        // Configure toolbar and toolbar tabs. If in landscape mode, we  configure tabs differntly.
305        final Toolbar toolbar = getView(R.id.toolbar);
306        setActionBar(toolbar);
307        final ViewPagerTabs portraitViewPagerTabs
308                = (ViewPagerTabs) findViewById(R.id.lists_pager_header);
309        ViewPagerTabs landscapeViewPagerTabs = null;
310        if (portraitViewPagerTabs ==  null) {
311            landscapeViewPagerTabs = (ViewPagerTabs) getLayoutInflater().inflate(
312                    R.layout.people_activity_tabs_lands, toolbar, /* attachToRoot = */ false);
313            mViewPagerTabs = landscapeViewPagerTabs;
314        } else {
315            mViewPagerTabs = portraitViewPagerTabs;
316        }
317        mViewPagerTabs.setViewPager(mTabPager);
318
319        final String FAVORITE_TAG = "tab-pager-favorite";
320        final String ALL_TAG = "tab-pager-all";
321
322        // Create the fragments and add as children of the view pager.
323        // The pager adapter will only change the visibility; it'll never create/destroy
324        // fragments.
325        // However, if it's after screen rotation, the fragments have been re-created by
326        // the fragment manager, so first see if there're already the target fragments
327        // existing.
328        mFavoritesFragment = (ContactTileListFragment)
329                fragmentManager.findFragmentByTag(FAVORITE_TAG);
330        mAllFragment = (DefaultContactBrowseListFragment)
331                fragmentManager.findFragmentByTag(ALL_TAG);
332
333        if (mFavoritesFragment == null) {
334            mFavoritesFragment = new ContactTileListFragment();
335            mAllFragment = new DefaultContactBrowseListFragment();
336
337            transaction.add(R.id.tab_pager, mFavoritesFragment, FAVORITE_TAG);
338            transaction.add(R.id.tab_pager, mAllFragment, ALL_TAG);
339        }
340
341        mFavoritesFragment.setListener(mFavoritesFragmentListener);
342
343        mAllFragment.setOnContactListActionListener(new ContactBrowserActionListener());
344
345        // Hide all fragments for now.  We adjust visibility when we get onSelectedTabChanged()
346        // from ActionBarAdapter.
347        transaction.hide(mFavoritesFragment);
348        transaction.hide(mAllFragment);
349
350        transaction.commitAllowingStateLoss();
351        fragmentManager.executePendingTransactions();
352
353        // Setting Properties after fragment is created
354        mFavoritesFragment.setDisplayType(DisplayType.STREQUENT);
355
356        mActionBarAdapter = new ActionBarAdapter(this, this, getActionBar(),
357                portraitViewPagerTabs, landscapeViewPagerTabs, toolbar);
358        mActionBarAdapter.initialize(savedState, mRequest);
359
360        // Add shadow under toolbar
361        ViewUtil.addRectangularOutlineProvider(findViewById(R.id.toolbar_parent), getResources());
362
363        // Configure action button
364        final View floatingActionButtonContainer = findViewById(
365                R.id.floating_action_button_container);
366        ViewUtil.setupFloatingActionButton(floatingActionButtonContainer, getResources());
367        final ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
368        floatingActionButton.setOnClickListener(this);
369
370        invalidateOptionsMenuIfNeeded();
371    }
372
373    @Override
374    protected void onStart() {
375        if (!mFragmentInitialized) {
376            mFragmentInitialized = true;
377            /* Configure fragments if we haven't.
378             *
379             * Note it's a one-shot initialization, so we want to do this in {@link #onCreate}.
380             *
381             * However, because this method may indirectly touch views in fragments but fragments
382             * created in {@link #configureContentView} using a {@link FragmentTransaction} will NOT
383             * have views until {@link Activity#onCreate} finishes (they would if they were inflated
384             * from a layout), we need to do it here in {@link #onStart()}.
385             *
386             * (When {@link Fragment#onCreateView} is called is different in the former case and
387             * in the latter case, unfortunately.)
388             *
389             * Also, we skip most of the work in it if the activity is a re-created one.
390             * (so the argument.)
391             */
392            configureFragments(!mIsRecreatedInstance);
393        }
394        super.onStart();
395    }
396
397    @Override
398    protected void onPause() {
399        mOptionsMenuContactsAvailable = false;
400        mProviderStatusWatcher.stop();
401        super.onPause();
402    }
403
404    @Override
405    protected void onResume() {
406        super.onResume();
407
408        mProviderStatusWatcher.start();
409        updateViewConfiguration(true);
410
411        // Re-register the listener, which may have been cleared when onSaveInstanceState was
412        // called.  See also: onSaveInstanceState
413        mActionBarAdapter.setListener(this);
414        mDisableOptionItemSelected = false;
415        if (mTabPager != null) {
416            mTabPager.setOnPageChangeListener(mTabPagerListener);
417        }
418        // Current tab may have changed since the last onSaveInstanceState().  Make sure
419        // the actual contents match the tab.
420        updateFragmentsVisibility();
421    }
422
423    @Override
424    protected void onStop() {
425        super.onStop();
426    }
427
428    @Override
429    protected void onDestroy() {
430        mProviderStatusWatcher.removeListener(this);
431
432        // Some of variables will be null if this Activity redirects Intent.
433        // See also onCreate() or other methods called during the Activity's initialization.
434        if (mActionBarAdapter != null) {
435            mActionBarAdapter.setListener(null);
436        }
437        if (mContactListFilterController != null) {
438            mContactListFilterController.removeListener(this);
439        }
440
441        super.onDestroy();
442    }
443
444    private void configureFragments(boolean fromRequest) {
445        if (fromRequest) {
446            ContactListFilter filter = null;
447            int actionCode = mRequest.getActionCode();
448            boolean searchMode = mRequest.isSearchMode();
449            final int tabToOpen;
450            switch (actionCode) {
451                case ContactsRequest.ACTION_ALL_CONTACTS:
452                    filter = ContactListFilter.createFilterWithType(
453                            ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
454                    tabToOpen = TabState.ALL;
455                    break;
456                case ContactsRequest.ACTION_CONTACTS_WITH_PHONES:
457                    filter = ContactListFilter.createFilterWithType(
458                            ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY);
459                    tabToOpen = TabState.ALL;
460                    break;
461
462                case ContactsRequest.ACTION_FREQUENT:
463                case ContactsRequest.ACTION_STREQUENT:
464                case ContactsRequest.ACTION_STARRED:
465                    tabToOpen = TabState.FAVORITES;
466                    break;
467                case ContactsRequest.ACTION_VIEW_CONTACT:
468                    tabToOpen = TabState.ALL;
469                    break;
470                default:
471                    tabToOpen = -1;
472                    break;
473            }
474            if (tabToOpen != -1) {
475                mActionBarAdapter.setCurrentTab(tabToOpen);
476            }
477
478            if (filter != null) {
479                mContactListFilterController.setContactListFilter(filter, false);
480                searchMode = false;
481            }
482
483            if (mRequest.getContactUri() != null) {
484                searchMode = false;
485            }
486
487            mActionBarAdapter.setSearchMode(searchMode);
488            configureContactListFragmentForRequest();
489        }
490
491        configureContactListFragment();
492
493        invalidateOptionsMenuIfNeeded();
494    }
495
496    @Override
497    public void onContactListFilterChanged() {
498        if (mAllFragment == null || !mAllFragment.isAdded()) {
499            return;
500        }
501
502        mAllFragment.setFilter(mContactListFilterController.getFilter());
503
504        invalidateOptionsMenuIfNeeded();
505    }
506
507    /**
508     * Handler for action bar actions.
509     */
510    @Override
511    public void onAction(int action) {
512        switch (action) {
513            case ActionBarAdapter.Listener.Action.START_SEARCH_MODE:
514                // Tell the fragments that we're in the search mode
515                configureFragments(false /* from request */);
516                updateFragmentsVisibility();
517                invalidateOptionsMenu();
518                break;
519            case ActionBarAdapter.Listener.Action.STOP_SEARCH_MODE:
520                setQueryTextToFragment("");
521                updateFragmentsVisibility();
522                invalidateOptionsMenu();
523                break;
524            case ActionBarAdapter.Listener.Action.CHANGE_SEARCH_QUERY:
525                final String queryString = mActionBarAdapter.getQueryString();
526                setQueryTextToFragment(queryString);
527                updateDebugOptionsVisibility(
528                        ENABLE_DEBUG_OPTIONS_HIDDEN_CODE.equals(queryString));
529                break;
530            default:
531                throw new IllegalStateException("Unkonwn ActionBarAdapter action: " + action);
532        }
533    }
534
535    @Override
536    public void onSelectedTabChanged() {
537        updateFragmentsVisibility();
538    }
539
540    @Override
541    public void onUpButtonPressed() {
542        onBackPressed();
543    }
544
545    private void updateDebugOptionsVisibility(boolean visible) {
546        if (mEnableDebugMenuOptions != visible) {
547            mEnableDebugMenuOptions = visible;
548            invalidateOptionsMenu();
549        }
550    }
551
552    /**
553     * Updates the fragment/view visibility according to the current mode, such as
554     * {@link ActionBarAdapter#isSearchMode()} and {@link ActionBarAdapter#getCurrentTab()}.
555     */
556    private void updateFragmentsVisibility() {
557        int tab = mActionBarAdapter.getCurrentTab();
558
559        if (mActionBarAdapter.isSearchMode()) {
560            mTabPagerAdapter.setSearchMode(true);
561        } else {
562            // No smooth scrolling if quitting from the search mode.
563            final boolean wasSearchMode = mTabPagerAdapter.isSearchMode();
564            mTabPagerAdapter.setSearchMode(false);
565            if (mTabPager.getCurrentItem() != tab) {
566                mTabPager.setCurrentItem(tab, !wasSearchMode);
567            }
568        }
569        invalidateOptionsMenu();
570        showEmptyStateForTab(tab);
571    }
572
573    private void showEmptyStateForTab(int tab) {
574        if (mContactsUnavailableFragment != null) {
575            switch (tab) {
576                case TabState.FAVORITES:
577                    mContactsUnavailableFragment.setMessageText(
578                            R.string.listTotalAllContactsZeroStarred, -1);
579                    break;
580                case TabState.ALL:
581                    mContactsUnavailableFragment.setMessageText(R.string.noContacts, -1);
582                    break;
583            }
584            // When using the mContactsUnavailableFragment the ViewPager doesn't contain two views.
585            // Therefore, we have to trick the ViewPagerTabs into thinking we have changed tabs
586            // when the mContactsUnavailableFragment changes. Otherwise the tab strip won't move.
587            mViewPagerTabs.onPageScrolled(tab, 0, 0);
588        }
589    }
590
591    private class TabPagerListener implements ViewPager.OnPageChangeListener {
592
593        // This package-protected constructor is here because of a possible compiler bug.
594        // PeopleActivity$1.class should be generated due to the private outer/inner class access
595        // needed here.  But for some reason, PeopleActivity$1.class is missing.
596        // Since $1 class is needed as a jvm work around to get access to the inner class,
597        // changing the constructor to package-protected or public will solve the problem.
598        // To verify whether $1 class is needed, javap PeopleActivity$TabPagerListener and look for
599        // references to PeopleActivity$1.
600        //
601        // When the constructor is private and PeopleActivity$1.class is missing, proguard will
602        // correctly catch this and throw warnings and error out the build on user/userdebug builds.
603        //
604        // All private inner classes below also need this fix.
605        TabPagerListener() {}
606
607        @Override
608        public void onPageScrollStateChanged(int state) {
609            if (!mTabPagerAdapter.isSearchMode()) {
610                mViewPagerTabs.onPageScrollStateChanged(state);
611            }
612        }
613
614        @Override
615        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
616            if (!mTabPagerAdapter.isSearchMode()) {
617                mViewPagerTabs.onPageScrolled(position, positionOffset, positionOffsetPixels);
618            }
619        }
620
621        @Override
622        public void onPageSelected(int position) {
623            // Make sure not in the search mode, in which case position != TabState.ordinal().
624            if (!mTabPagerAdapter.isSearchMode()) {
625                mActionBarAdapter.setCurrentTab(position, false);
626                mViewPagerTabs.onPageSelected(position);
627                showEmptyStateForTab(position);
628                invalidateOptionsMenu();
629            }
630        }
631    }
632
633    /**
634     * Adapter for the {@link ViewPager}.  Unlike {@link FragmentPagerAdapter},
635     * {@link #instantiateItem} returns existing fragments, and {@link #instantiateItem}/
636     * {@link #destroyItem} show/hide fragments instead of attaching/detaching.
637     *
638     * In search mode, we always show the "all" fragment, and disable the swipe.  We change the
639     * number of items to 1 to disable the swipe.
640     *
641     * TODO figure out a more straight way to disable swipe.
642     */
643    private class TabPagerAdapter extends PagerAdapter {
644        private final FragmentManager mFragmentManager;
645        private FragmentTransaction mCurTransaction = null;
646
647        private boolean mTabPagerAdapterSearchMode;
648
649        private Fragment mCurrentPrimaryItem;
650
651        public TabPagerAdapter() {
652            mFragmentManager = getFragmentManager();
653        }
654
655        public boolean isSearchMode() {
656            return mTabPagerAdapterSearchMode;
657        }
658
659        public void setSearchMode(boolean searchMode) {
660            if (searchMode == mTabPagerAdapterSearchMode) {
661                return;
662            }
663            mTabPagerAdapterSearchMode = searchMode;
664            notifyDataSetChanged();
665        }
666
667        @Override
668        public int getCount() {
669            return mTabPagerAdapterSearchMode ? 1 : TabState.COUNT;
670        }
671
672        /** Gets called when the number of items changes. */
673        @Override
674        public int getItemPosition(Object object) {
675            if (mTabPagerAdapterSearchMode) {
676                if (object == mAllFragment) {
677                    return 0; // Only 1 page in search mode
678                }
679            } else {
680                if (object == mFavoritesFragment) {
681                    return getTabPositionForTextDirection(TabState.FAVORITES);
682                }
683                if (object == mAllFragment) {
684                    return getTabPositionForTextDirection(TabState.ALL);
685                }
686            }
687            return POSITION_NONE;
688        }
689
690        @Override
691        public void startUpdate(ViewGroup container) {
692        }
693
694        private Fragment getFragment(int position) {
695            position = getTabPositionForTextDirection(position);
696            if (mTabPagerAdapterSearchMode) {
697                if (position != 0) {
698                    // This has only been observed in monkey tests.
699                    // Let's log this issue, but not crash
700                    Log.w(TAG, "Request fragment at position=" + position + ", eventhough we " +
701                            "are in search mode");
702                }
703                return mAllFragment;
704            } else {
705                if (position == TabState.FAVORITES) {
706                    return mFavoritesFragment;
707                } else if (position == TabState.ALL) {
708                    return mAllFragment;
709                }
710            }
711            throw new IllegalArgumentException("position: " + position);
712        }
713
714        @Override
715        public Object instantiateItem(ViewGroup container, int position) {
716            if (mCurTransaction == null) {
717                mCurTransaction = mFragmentManager.beginTransaction();
718            }
719            Fragment f = getFragment(position);
720            mCurTransaction.show(f);
721
722            // Non primary pages are not visible.
723            f.setUserVisibleHint(f == mCurrentPrimaryItem);
724            return f;
725        }
726
727        @Override
728        public void destroyItem(ViewGroup container, int position, Object object) {
729            if (mCurTransaction == null) {
730                mCurTransaction = mFragmentManager.beginTransaction();
731            }
732            mCurTransaction.hide((Fragment) object);
733        }
734
735        @Override
736        public void finishUpdate(ViewGroup container) {
737            if (mCurTransaction != null) {
738                mCurTransaction.commitAllowingStateLoss();
739                mCurTransaction = null;
740                mFragmentManager.executePendingTransactions();
741            }
742        }
743
744        @Override
745        public boolean isViewFromObject(View view, Object object) {
746            return ((Fragment) object).getView() == view;
747        }
748
749        @Override
750        public void setPrimaryItem(ViewGroup container, int position, Object object) {
751            Fragment fragment = (Fragment) object;
752            if (mCurrentPrimaryItem != fragment) {
753                if (mCurrentPrimaryItem != null) {
754                    mCurrentPrimaryItem.setUserVisibleHint(false);
755                }
756                if (fragment != null) {
757                    fragment.setUserVisibleHint(true);
758                }
759                mCurrentPrimaryItem = fragment;
760            }
761        }
762
763        @Override
764        public Parcelable saveState() {
765            return null;
766        }
767
768        @Override
769        public void restoreState(Parcelable state, ClassLoader loader) {
770        }
771
772        @Override
773        public CharSequence getPageTitle(int position) {
774            return mTabTitles[position];
775        }
776    }
777
778    private void setQueryTextToFragment(String query) {
779        mAllFragment.setQueryString(query, true);
780        mAllFragment.setVisibleScrollbarEnabled(!mAllFragment.isSearchMode());
781    }
782
783    private void configureContactListFragmentForRequest() {
784        Uri contactUri = mRequest.getContactUri();
785        if (contactUri != null) {
786            mAllFragment.setSelectedContactUri(contactUri);
787        }
788
789        mAllFragment.setFilter(mContactListFilterController.getFilter());
790        setQueryTextToFragment(mActionBarAdapter.getQueryString());
791
792        if (mRequest.isDirectorySearchEnabled()) {
793            mAllFragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_DEFAULT);
794        } else {
795            mAllFragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE);
796        }
797    }
798
799    private void configureContactListFragment() {
800        // Filter may be changed when this Activity is in background.
801        mAllFragment.setFilter(mContactListFilterController.getFilter());
802
803        mAllFragment.setVerticalScrollbarPosition(getScrollBarPosition());
804        mAllFragment.setSelectionVisible(false);
805    }
806
807    private int getScrollBarPosition() {
808        return isRTL() ? View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT;
809    }
810
811    private boolean isRTL() {
812        final Locale locale = Locale.getDefault();
813        return TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL;
814    }
815
816    @Override
817    public void onProviderStatusChange() {
818        updateViewConfiguration(false);
819    }
820
821    private void updateViewConfiguration(boolean forceUpdate) {
822        ProviderStatusWatcher.Status providerStatus = mProviderStatusWatcher.getProviderStatus();
823        if (!forceUpdate && (mProviderStatus != null)
824                && (providerStatus.status == mProviderStatus.status)) return;
825        mProviderStatus = providerStatus;
826
827        View contactsUnavailableView = findViewById(R.id.contacts_unavailable_view);
828
829        if (mProviderStatus.status == ProviderStatus.STATUS_NORMAL) {
830            // Ensure that the mTabPager is visible; we may have made it invisible below.
831            contactsUnavailableView.setVisibility(View.GONE);
832            if (mTabPager != null) {
833                mTabPager.setVisibility(View.VISIBLE);
834            }
835
836            if (mAllFragment != null) {
837                mAllFragment.setEnabled(true);
838            }
839        } else {
840            // If there are no accounts on the device and we should show the "no account" prompt
841            // (based on {@link SharedPreferences}), then launch the account setup activity so the
842            // user can sign-in or create an account.
843            //
844            // Also check for ability to modify accounts.  In limited user mode, you can't modify
845            // accounts so there is no point sending users to account setup activity.
846            final UserManager userManager = UserManager.get(this);
847            final boolean disallowModifyAccounts = userManager.getUserRestrictions().getBoolean(
848                    UserManager.DISALLOW_MODIFY_ACCOUNTS);
849            if (!disallowModifyAccounts && !areContactWritableAccountsAvailable() &&
850                    AccountPromptUtils.shouldShowAccountPrompt(this)) {
851                AccountPromptUtils.neverShowAccountPromptAgain(this);
852                AccountPromptUtils.launchAccountPrompt(this);
853                return;
854            }
855
856            // Otherwise, continue setting up the page so that the user can still use the app
857            // without an account.
858            if (mAllFragment != null) {
859                mAllFragment.setEnabled(false);
860            }
861            if (mContactsUnavailableFragment == null) {
862                mContactsUnavailableFragment = new ContactsUnavailableFragment();
863                mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
864                        new ContactsUnavailableFragmentListener());
865                getFragmentManager().beginTransaction()
866                        .replace(R.id.contacts_unavailable_container, mContactsUnavailableFragment)
867                        .commitAllowingStateLoss();
868            }
869            mContactsUnavailableFragment.updateStatus(mProviderStatus);
870
871            // Show the contactsUnavailableView, and hide the mTabPager so that we don't
872            // see it sliding in underneath the contactsUnavailableView at the edges.
873            contactsUnavailableView.setVisibility(View.VISIBLE);
874            if (mTabPager != null) {
875                mTabPager.setVisibility(View.GONE);
876            }
877
878            showEmptyStateForTab(mActionBarAdapter.getCurrentTab());
879        }
880
881        invalidateOptionsMenuIfNeeded();
882    }
883
884    private final class ContactBrowserActionListener implements OnContactBrowserActionListener {
885        ContactBrowserActionListener() {}
886
887        @Override
888        public void onSelectionChange() {
889
890        }
891
892        @Override
893        public void onViewContactAction(Uri contactLookupUri) {
894            Intent intent = QuickContact.composeQuickContactsIntent(PeopleActivity.this,
895                    (Rect) null, contactLookupUri, QuickContactActivity.MODE_FULLY_EXPANDED, null);
896            startActivity(intent);
897        }
898
899        @Override
900        public void onDeleteContactAction(Uri contactUri) {
901            ContactDeletionInteraction.start(PeopleActivity.this, contactUri, false);
902        }
903
904        @Override
905        public void onFinishAction() {
906            onBackPressed();
907        }
908
909        @Override
910        public void onInvalidSelection() {
911            ContactListFilter filter;
912            ContactListFilter currentFilter = mAllFragment.getFilter();
913            if (currentFilter != null
914                    && currentFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
915                filter = ContactListFilter.createFilterWithType(
916                        ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
917                mAllFragment.setFilter(filter);
918            } else {
919                filter = ContactListFilter.createFilterWithType(
920                        ContactListFilter.FILTER_TYPE_SINGLE_CONTACT);
921                mAllFragment.setFilter(filter, false);
922            }
923            mContactListFilterController.setContactListFilter(filter, true);
924        }
925    }
926
927    private class ContactsUnavailableFragmentListener
928            implements OnContactsUnavailableActionListener {
929        ContactsUnavailableFragmentListener() {}
930
931        @Override
932        public void onCreateNewContactAction() {
933            startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
934        }
935
936        @Override
937        public void onAddAccountAction() {
938            Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
939            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
940            intent.putExtra(Settings.EXTRA_AUTHORITIES,
941                    new String[] { ContactsContract.AUTHORITY });
942            startActivity(intent);
943        }
944
945        @Override
946        public void onImportContactsFromFileAction() {
947            ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable(),
948                    PeopleActivity.class);
949        }
950
951        @Override
952        public void onFreeInternalStorageAction() {
953            startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
954        }
955    }
956
957    private final class StrequentContactListFragmentListener
958            implements ContactTileListFragment.Listener {
959        StrequentContactListFragmentListener() {}
960
961        @Override
962        public void onContactSelected(Uri contactUri, Rect targetRect) {
963            Intent intent = QuickContact.composeQuickContactsIntent(PeopleActivity.this,
964                    targetRect, contactUri, QuickContactActivity.MODE_FULLY_EXPANDED, null);
965            startActivity(intent);
966        }
967
968        @Override
969        public void onCallNumberDirectly(String phoneNumber) {
970            // No need to call phone number directly from People app.
971            Log.w(TAG, "unexpected invocation of onCallNumberDirectly()");
972        }
973    }
974
975    @Override
976    public boolean onCreateOptionsMenu(Menu menu) {
977        if (!areContactsAvailable()) {
978            // If contacts aren't available, hide all menu items.
979            return false;
980        }
981        super.onCreateOptionsMenu(menu);
982
983        MenuInflater inflater = getMenuInflater();
984        inflater.inflate(R.menu.people_options, menu);
985
986        return true;
987    }
988
989    private void invalidateOptionsMenuIfNeeded() {
990        if (isOptionsMenuChanged()) {
991            invalidateOptionsMenu();
992        }
993    }
994
995    public boolean isOptionsMenuChanged() {
996        if (mOptionsMenuContactsAvailable != areContactsAvailable()) {
997            return true;
998        }
999
1000        if (mAllFragment != null && mAllFragment.isOptionsMenuChanged()) {
1001            return true;
1002        }
1003
1004        return false;
1005    }
1006
1007    @Override
1008    public boolean onPrepareOptionsMenu(Menu menu) {
1009        mOptionsMenuContactsAvailable = areContactsAvailable();
1010        if (!mOptionsMenuContactsAvailable) {
1011            return false;
1012        }
1013
1014        // Get references to individual menu items in the menu
1015        final MenuItem contactsFilterMenu = menu.findItem(R.id.menu_contacts_filter);
1016        final MenuItem clearFrequentsMenu = menu.findItem(R.id.menu_clear_frequents);
1017        final MenuItem helpMenu = menu.findItem(R.id.menu_help);
1018
1019        final boolean isSearchMode = mActionBarAdapter.isSearchMode();
1020        if (isSearchMode) {
1021            contactsFilterMenu.setVisible(false);
1022            clearFrequentsMenu.setVisible(false);
1023            helpMenu.setVisible(false);
1024        } else {
1025            switch (mActionBarAdapter.getCurrentTab()) {
1026                case TabState.FAVORITES:
1027                    contactsFilterMenu.setVisible(false);
1028                    clearFrequentsMenu.setVisible(hasFrequents());
1029                    break;
1030                case TabState.ALL:
1031                    contactsFilterMenu.setVisible(true);
1032                    clearFrequentsMenu.setVisible(false);
1033                    break;
1034            }
1035            HelpUtils.prepareHelpMenuItem(this, helpMenu, R.string.help_url_people_main);
1036        }
1037        final boolean showMiscOptions = !isSearchMode;
1038        makeMenuItemVisible(menu, R.id.menu_search, showMiscOptions);
1039        makeMenuItemVisible(menu, R.id.menu_import_export, showMiscOptions);
1040        makeMenuItemVisible(menu, R.id.menu_accounts, showMiscOptions);
1041        makeMenuItemVisible(menu, R.id.menu_settings,
1042                showMiscOptions && !ContactsPreferenceActivity.isEmpty(this));
1043
1044        // Debug options need to be visible even in search mode.
1045        makeMenuItemVisible(menu, R.id.export_database, mEnableDebugMenuOptions);
1046
1047        return true;
1048    }
1049
1050    /**
1051     * Returns whether there are any frequently contacted people being displayed
1052     * @return
1053     */
1054    private boolean hasFrequents() {
1055        return mFavoritesFragment.hasFrequents();
1056    }
1057
1058    private void makeMenuItemVisible(Menu menu, int itemId, boolean visible) {
1059        MenuItem item =menu.findItem(itemId);
1060        if (item != null) {
1061            item.setVisible(visible);
1062        }
1063    }
1064
1065    @Override
1066    public boolean onOptionsItemSelected(MenuItem item) {
1067        if (mDisableOptionItemSelected) {
1068            return false;
1069        }
1070
1071        switch (item.getItemId()) {
1072            case android.R.id.home: {
1073                // The home icon on the action bar is pressed
1074                if (mActionBarAdapter.isUpShowing()) {
1075                    // "UP" icon press -- should be treated as "back".
1076                    onBackPressed();
1077                }
1078                return true;
1079            }
1080            case R.id.menu_settings: {
1081                final Intent intent = new Intent(this, ContactsPreferenceActivity.class);
1082                // Since there is only one section right now, make sure it is selected on
1083                // small screens.
1084                intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT,
1085                        DisplayOptionsPreferenceFragment.class.getName());
1086                // By default, the title of the activity should be equivalent to the fragment
1087                // title. We set this argument to avoid this. Because of a bug, the following
1088                // line isn't necessary. But, once the bug is fixed this may become necessary.
1089                // b/5045558 refers to this issue, as well as another.
1090                intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE,
1091                        R.string.activity_title_settings);
1092                startActivity(intent);
1093                return true;
1094            }
1095            case R.id.menu_contacts_filter: {
1096                AccountFilterUtil.startAccountFilterActivityForResult(
1097                        this, SUBACTIVITY_ACCOUNT_FILTER,
1098                        mContactListFilterController.getFilter());
1099                return true;
1100            }
1101            case R.id.menu_search: {
1102                onSearchRequested();
1103                return true;
1104            }
1105            case R.id.menu_import_export: {
1106                ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable(),
1107                        PeopleActivity.class);
1108                return true;
1109            }
1110            case R.id.menu_clear_frequents: {
1111                ClearFrequentsDialog.show(getFragmentManager());
1112                return true;
1113            }
1114            case R.id.menu_accounts: {
1115                final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
1116                intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] {
1117                    ContactsContract.AUTHORITY
1118                });
1119                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
1120                startActivity(intent);
1121                return true;
1122            }
1123            case R.id.export_database: {
1124                final Intent intent = new Intent("com.android.providers.contacts.DUMP_DATABASE");
1125                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
1126                startActivity(intent);
1127                return true;
1128            }
1129        }
1130        return false;
1131    }
1132
1133    @Override
1134    public boolean onSearchRequested() { // Search key pressed.
1135        mActionBarAdapter.setSearchMode(true);
1136        return true;
1137    }
1138
1139    @Override
1140    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1141        switch (requestCode) {
1142            case SUBACTIVITY_ACCOUNT_FILTER: {
1143                AccountFilterUtil.handleAccountFilterResult(
1144                        mContactListFilterController, resultCode, data);
1145                break;
1146            }
1147
1148            // TODO: Using the new startActivityWithResultFromFragment API this should not be needed
1149            // anymore
1150            case ContactEntryListFragment.ACTIVITY_REQUEST_CODE_PICKER:
1151                if (resultCode == RESULT_OK) {
1152                    mAllFragment.onPickerResult(data);
1153                }
1154
1155// TODO fix or remove multipicker code
1156//                else if (resultCode == RESULT_CANCELED && mMode == MODE_PICK_MULTIPLE_PHONES) {
1157//                    // Finish the activity if the sub activity was canceled as back key is used
1158//                    // to confirm user selection in MODE_PICK_MULTIPLE_PHONES.
1159//                    finish();
1160//                }
1161//                break;
1162        }
1163    }
1164
1165    @Override
1166    public boolean onKeyDown(int keyCode, KeyEvent event) {
1167        // TODO move to the fragment
1168        switch (keyCode) {
1169//            case KeyEvent.KEYCODE_CALL: {
1170//                if (callSelection()) {
1171//                    return true;
1172//                }
1173//                break;
1174//            }
1175
1176            case KeyEvent.KEYCODE_DEL: {
1177                if (deleteSelection()) {
1178                    return true;
1179                }
1180                break;
1181            }
1182            default: {
1183                // Bring up the search UI if the user starts typing
1184                final int unicodeChar = event.getUnicodeChar();
1185                if ((unicodeChar != 0)
1186                        // If COMBINING_ACCENT is set, it's not a unicode character.
1187                        && ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) == 0)
1188                        && !Character.isWhitespace(unicodeChar)) {
1189                    String query = new String(new int[]{ unicodeChar }, 0, 1);
1190                    if (!mActionBarAdapter.isSearchMode()) {
1191                        mActionBarAdapter.setQueryString(query);
1192                        mActionBarAdapter.setSearchMode(true);
1193                        return true;
1194                    }
1195                }
1196            }
1197        }
1198
1199        return super.onKeyDown(keyCode, event);
1200    }
1201
1202    @Override
1203    public void onBackPressed() {
1204        if (mActionBarAdapter.isSearchMode()) {
1205            mActionBarAdapter.setSearchMode(false);
1206        } else {
1207            super.onBackPressed();
1208        }
1209    }
1210
1211    private boolean deleteSelection() {
1212        // TODO move to the fragment
1213//        if (mActionCode == ContactsRequest.ACTION_DEFAULT) {
1214//            final int position = mListView.getSelectedItemPosition();
1215//            if (position != ListView.INVALID_POSITION) {
1216//                Uri contactUri = getContactUri(position);
1217//                if (contactUri != null) {
1218//                    doContactDelete(contactUri);
1219//                    return true;
1220//                }
1221//            }
1222//        }
1223        return false;
1224    }
1225
1226    @Override
1227    protected void onSaveInstanceState(Bundle outState) {
1228        super.onSaveInstanceState(outState);
1229        mActionBarAdapter.onSaveInstanceState(outState);
1230
1231        // Clear the listener to make sure we don't get callbacks after onSaveInstanceState,
1232        // in order to avoid doing fragment transactions after it.
1233        // TODO Figure out a better way to deal with the issue.
1234        mDisableOptionItemSelected = true;
1235        mActionBarAdapter.setListener(null);
1236        if (mTabPager != null) {
1237            mTabPager.setOnPageChangeListener(null);
1238        }
1239    }
1240
1241    @Override
1242    protected void onRestoreInstanceState(Bundle savedInstanceState) {
1243        super.onRestoreInstanceState(savedInstanceState);
1244        // In our own lifecycle, the focus is saved and restore but later taken away by the
1245        // ViewPager. As a hack, we force focus on the SearchView if we know that we are searching.
1246        // This fixes the keyboard going away on screen rotation
1247        if (mActionBarAdapter.isSearchMode()) {
1248            mActionBarAdapter.setFocusOnSearchView();
1249        }
1250    }
1251
1252    @Override
1253    public DialogManager getDialogManager() {
1254        return mDialogManager;
1255    }
1256
1257    @Override
1258    public void onClick(View view) {
1259        switch (view.getId()) {
1260            case R.id.floating_action_button:
1261                Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
1262                Bundle extras = getIntent().getExtras();
1263                if (extras != null) {
1264                    intent.putExtras(extras);
1265                }
1266                startActivity(intent);
1267                break;
1268        default:
1269            Log.wtf(TAG, "Unexpected onClick event from " + view);
1270        }
1271    }
1272
1273    /**
1274     * Returns the tab position adjusted for the text direction.
1275     */
1276    private int getTabPositionForTextDirection(int position) {
1277        if (isRTL()) {
1278            return TabState.COUNT - 1 - position;
1279        }
1280        return position;
1281    }
1282}
1283