PeopleActivity.java revision 2fc4de31fe2d6a261d3eeaff69ea944a5383003e
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 com.android.contacts.ContactLoader;
20import com.android.contacts.ContactSaveService;
21import com.android.contacts.ContactsActivity;
22import com.android.contacts.ContactsUtils;
23import com.android.contacts.R;
24import com.android.contacts.activities.ActionBarAdapter.TabState;
25import com.android.contacts.detail.ContactDetailFragment;
26import com.android.contacts.detail.ContactDetailLayoutController;
27import com.android.contacts.detail.ContactDetailUpdatesFragment;
28import com.android.contacts.detail.ContactLoaderFragment;
29import com.android.contacts.detail.ContactLoaderFragment.ContactLoaderFragmentListener;
30import com.android.contacts.dialog.ClearFrequentsDialog;
31import com.android.contacts.group.GroupBrowseListFragment;
32import com.android.contacts.group.GroupBrowseListFragment.OnGroupBrowserActionListener;
33import com.android.contacts.group.GroupDetailFragment;
34import com.android.contacts.interactions.ContactDeletionInteraction;
35import com.android.contacts.interactions.ImportExportDialogFragment;
36import com.android.contacts.interactions.PhoneNumberInteraction;
37import com.android.contacts.list.ContactBrowseListFragment;
38import com.android.contacts.list.ContactEntryListFragment;
39import com.android.contacts.list.ContactListFilter;
40import com.android.contacts.list.ContactListFilterController;
41import com.android.contacts.list.ContactTileAdapter.DisplayType;
42import com.android.contacts.list.ContactTileFrequentFragment;
43import com.android.contacts.list.ContactTileListFragment;
44import com.android.contacts.list.ContactsIntentResolver;
45import com.android.contacts.list.ContactsRequest;
46import com.android.contacts.list.ContactsUnavailableFragment;
47import com.android.contacts.list.DefaultContactBrowseListFragment;
48import com.android.contacts.list.DirectoryListLoader;
49import com.android.contacts.list.OnContactBrowserActionListener;
50import com.android.contacts.list.OnContactsUnavailableActionListener;
51import com.android.contacts.list.ProviderStatusLoader;
52import com.android.contacts.list.ProviderStatusLoader.ProviderStatusListener;
53import com.android.contacts.model.AccountTypeManager;
54import com.android.contacts.model.AccountWithDataSet;
55import com.android.contacts.preference.ContactsPreferenceActivity;
56import com.android.contacts.preference.DisplayOptionsPreferenceFragment;
57import com.android.contacts.util.AccountFilterUtil;
58import com.android.contacts.util.AccountPromptUtils;
59import com.android.contacts.util.AccountsListAdapter;
60import com.android.contacts.util.UriUtils;
61import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
62import com.android.contacts.util.Constants;
63import com.android.contacts.util.DialogManager;
64import com.android.contacts.util.PhoneCapabilityTester;
65import com.android.contacts.widget.TransitionAnimationView;
66
67import android.app.Fragment;
68import android.app.FragmentManager;
69import android.app.FragmentTransaction;
70import android.content.ActivityNotFoundException;
71import android.content.ContentValues;
72import android.content.Intent;
73import android.graphics.Rect;
74import android.net.Uri;
75import android.os.Bundle;
76import android.os.Handler;
77import android.os.Parcelable;
78import android.preference.PreferenceActivity;
79import android.provider.ContactsContract;
80import android.provider.ContactsContract.Contacts;
81import android.provider.ContactsContract.Intents;
82import android.provider.ContactsContract.ProviderStatus;
83import android.provider.ContactsContract.QuickContact;
84import android.provider.Settings;
85import android.support.v13.app.FragmentPagerAdapter;
86import android.support.v4.view.PagerAdapter;
87import android.support.v4.view.ViewPager;
88import android.util.Log;
89import android.view.KeyCharacterMap;
90import android.view.KeyEvent;
91import android.view.Menu;
92import android.view.MenuInflater;
93import android.view.MenuItem;
94import android.view.MenuItem.OnMenuItemClickListener;
95import android.view.View;
96import android.view.View.OnClickListener;
97import android.view.ViewGroup;
98import android.widget.AdapterView;
99import android.widget.AdapterView.OnItemClickListener;
100import android.widget.ListPopupWindow;
101import android.widget.Toast;
102
103import java.util.ArrayList;
104import java.util.List;
105import java.util.concurrent.atomic.AtomicInteger;
106
107/**
108 * Displays a list to browse contacts. For xlarge screens, this also displays a detail-pane on
109 * the right.
110 */
111public class PeopleActivity extends ContactsActivity
112        implements View.OnCreateContextMenuListener, ActionBarAdapter.Listener,
113        DialogManager.DialogShowingViewActivity,
114        ContactListFilterController.ContactListFilterListener, ProviderStatusListener {
115
116    private static final String TAG = "PeopleActivity";
117
118    /** Shows a toogle button for hiding/showing updates. Don't submit with true */
119    private static final boolean DEBUG_TRANSITIONS = false;
120
121    // These values needs to start at 2. See {@link ContactEntryListFragment}.
122    private static final int SUBACTIVITY_NEW_CONTACT = 2;
123    private static final int SUBACTIVITY_EDIT_CONTACT = 3;
124    private static final int SUBACTIVITY_NEW_GROUP = 4;
125    private static final int SUBACTIVITY_EDIT_GROUP = 5;
126    private static final int SUBACTIVITY_ACCOUNT_FILTER = 6;
127
128    private final DialogManager mDialogManager = new DialogManager(this);
129
130    private ContactsIntentResolver mIntentResolver;
131    private ContactsRequest mRequest;
132
133    private ActionBarAdapter mActionBarAdapter;
134
135    private ContactDetailFragment mContactDetailFragment;
136
137    private ContactLoaderFragment mContactDetailLoaderFragment;
138    private final ContactDetailLoaderFragmentListener mContactDetailLoaderFragmentListener =
139            new ContactDetailLoaderFragmentListener();
140
141    private GroupDetailFragment mGroupDetailFragment;
142    private final GroupDetailFragmentListener mGroupDetailFragmentListener =
143            new GroupDetailFragmentListener();
144
145    private ContactTileListFragment.Listener mFavoritesFragmentListener =
146            new StrequentContactListFragmentListener();
147
148    private ContactListFilterController mContactListFilterController;
149
150    private ContactsUnavailableFragment mContactsUnavailableFragment;
151    private ProviderStatusLoader mProviderStatusLoader;
152    private int mProviderStatus = -1;
153
154    private boolean mOptionsMenuContactsAvailable;
155
156    /**
157     * Showing a list of Contacts. Also used for showing search results in search mode.
158     */
159    private DefaultContactBrowseListFragment mAllFragment;
160    private ContactTileListFragment mFavoritesFragment;
161    private ContactTileFrequentFragment mFrequentFragment;
162    private GroupBrowseListFragment mGroupsFragment;
163
164    private View mFavoritesView;
165    private View mBrowserView;
166    private TransitionAnimationView mContactDetailsView;
167    private TransitionAnimationView mGroupDetailsView;
168
169    private View mAddGroupImageView;
170
171    /** ViewPager for swipe, used only on the phone (i.e. one-pane mode) */
172    private ViewPager mTabPager;
173    private TabPagerAdapter mTabPagerAdapter;
174    private final TabPagerListener mTabPagerListener = new TabPagerListener();
175
176    private ContactDetailLayoutController mContactDetailLayoutController;
177
178    private final Handler mHandler = new Handler();
179
180    /**
181     * True if this activity instance is a re-created one.  i.e. set true after orientation change.
182     * This is set in {@link #onCreate} for later use in {@link #onStart}.
183     */
184    private boolean mIsRecreatedInstance;
185
186    /**
187     * If {@link #configureFragments(boolean)} is already called.  Used to avoid calling it twice
188     * in {@link #onStart}.
189     * (This initialization only needs to be done once in onStart() when the Activity was just
190     * created from scratch -- i.e. onCreate() was just called)
191     */
192    private boolean mFragmentInitialized;
193
194    /**
195     * Whether or not the current contact filter is valid or not. We need to do a check on
196     * start of the app to verify that the user is not in single contact mode. If so, we should
197     * dynamically change the filter, unless the incoming intent specifically requested a contact
198     * that should be displayed in that mode.
199     */
200    private boolean mCurrentFilterIsValid;
201
202    /** Sequential ID assigned to each instance; used for logging */
203    private final int mInstanceId;
204    private static final AtomicInteger sNextInstanceId = new AtomicInteger();
205
206    public PeopleActivity() {
207        mInstanceId = sNextInstanceId.getAndIncrement();
208        mIntentResolver = new ContactsIntentResolver(this);
209        mProviderStatusLoader = new ProviderStatusLoader(this);
210    }
211
212    @Override
213    public String toString() {
214        // Shown on logcat
215        return String.format("%s@%d", getClass().getSimpleName(), mInstanceId);
216    }
217
218    public boolean areContactsAvailable() {
219        return mProviderStatus == ProviderStatus.STATUS_NORMAL;
220    }
221
222    private boolean areContactWritableAccountsAvailable() {
223        return ContactsUtils.areContactWritableAccountsAvailable(this);
224    }
225
226    private boolean areGroupWritableAccountsAvailable() {
227        return ContactsUtils.areGroupWritableAccountsAvailable(this);
228    }
229
230    /**
231     * Initialize fragments that are (or may not be) in the layout.
232     *
233     * For the fragments that are in the layout, we initialize them in
234     * {@link #createViewsAndFragments(Bundle)} after inflating the layout.
235     *
236     * However, there are special fragments which may not be in the layout, so we have to do the
237     * initialization here.
238     * The target fragments are:
239     * - {@link ContactDetailFragment} and {@link ContactDetailUpdatesFragment}:  They may not be
240     *   in the layout depending on the configuration.  (i.e. portrait)
241     * - {@link ContactsUnavailableFragment}: We always create it at runtime.
242     */
243    @Override
244    public void onAttachFragment(Fragment fragment) {
245        if (fragment instanceof ContactDetailFragment) {
246            mContactDetailFragment = (ContactDetailFragment) fragment;
247        } else if (fragment instanceof ContactsUnavailableFragment) {
248            mContactsUnavailableFragment = (ContactsUnavailableFragment)fragment;
249            mContactsUnavailableFragment.setProviderStatusLoader(mProviderStatusLoader);
250            mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
251                    new ContactsUnavailableFragmentListener());
252        }
253    }
254
255    @Override
256    protected void onCreate(Bundle savedState) {
257        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
258            Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate start");
259        }
260        super.onCreate(savedState);
261
262        if (!processIntent(false)) {
263            finish();
264            return;
265        }
266
267        mContactListFilterController = ContactListFilterController.getInstance(this);
268        mContactListFilterController.checkFilterValidity(false);
269        mContactListFilterController.addListener(this);
270
271        mIsRecreatedInstance = (savedState != null);
272        createViewsAndFragments(savedState);
273        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
274            Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate finish");
275        }
276    }
277
278    @Override
279    protected void onNewIntent(Intent intent) {
280        setIntent(intent);
281        if (!processIntent(true)) {
282            finish();
283            return;
284        }
285        mActionBarAdapter.initialize(null, mRequest);
286
287        mContactListFilterController.checkFilterValidity(false);
288        mCurrentFilterIsValid = true;
289
290        // Re-configure fragments.
291        configureFragments(true /* from request */);
292        invalidateOptionsMenuIfNeeded();
293    }
294
295    /**
296     * Resolve the intent and initialize {@link #mRequest}, and launch another activity if redirect
297     * is needed.
298     *
299     * @param forNewIntent set true if it's called from {@link #onNewIntent(Intent)}.
300     * @return {@code true} if {@link PeopleActivity} should continue running.  {@code false}
301     *         if it shouldn't, in which case the caller should finish() itself and shouldn't do
302     *         farther initialization.
303     */
304    private boolean processIntent(boolean forNewIntent) {
305        // Extract relevant information from the intent
306        mRequest = mIntentResolver.resolveIntent(getIntent());
307        if (Log.isLoggable(TAG, Log.DEBUG)) {
308            Log.d(TAG, this + " processIntent: forNewIntent=" + forNewIntent
309                    + " intent=" + getIntent() + " request=" + mRequest);
310        }
311        if (!mRequest.isValid()) {
312            setResult(RESULT_CANCELED);
313            return false;
314        }
315
316        Intent redirect = mRequest.getRedirectIntent();
317        if (redirect != null) {
318            // Need to start a different activity
319            startActivity(redirect);
320            return false;
321        }
322
323        if (mRequest.getActionCode() == ContactsRequest.ACTION_VIEW_CONTACT
324                && !PhoneCapabilityTester.isUsingTwoPanes(this)) {
325            redirect = new Intent(this, ContactDetailActivity.class);
326            redirect.setAction(Intent.ACTION_VIEW);
327            redirect.setData(mRequest.getContactUri());
328            startActivity(redirect);
329            return false;
330        }
331        setTitle(mRequest.getActivityTitle());
332        return true;
333    }
334
335    private void createViewsAndFragments(Bundle savedState) {
336        setContentView(R.layout.people_activity);
337
338        final FragmentManager fragmentManager = getFragmentManager();
339
340        // Hide all tabs (the current tab will later be reshown once a tab is selected)
341        final FragmentTransaction transaction = fragmentManager.beginTransaction();
342
343        // Prepare the fragments which are used both on 1-pane and on 2-pane.
344        boolean isUsingTwoPanes = PhoneCapabilityTester.isUsingTwoPanes(this);
345        if (isUsingTwoPanes) {
346            mFavoritesFragment = getFragment(R.id.favorites_fragment);
347            mAllFragment = getFragment(R.id.all_fragment);
348            mGroupsFragment = getFragment(R.id.groups_fragment);
349        } else {
350            mTabPager = getView(R.id.tab_pager);
351            mTabPagerAdapter = new TabPagerAdapter();
352            mTabPager.setAdapter(mTabPagerAdapter);
353            mTabPager.setOnPageChangeListener(mTabPagerListener);
354
355            final String FAVORITE_TAG = "tab-pager-favorite";
356            final String ALL_TAG = "tab-pager-all";
357            final String GROUPS_TAG = "tab-pager-groups";
358
359            // Create the fragments and add as children of the view pager.
360            // The pager adapter will only change the visibility; it'll never create/destroy
361            // fragments.
362            // However, if it's after screen rotation, the fragments have been re-created by
363            // the fragment manager, so first see if there're already the target fragments
364            // existing.
365            mFavoritesFragment = (ContactTileListFragment)
366                    fragmentManager.findFragmentByTag(FAVORITE_TAG);
367            mAllFragment = (DefaultContactBrowseListFragment)
368                    fragmentManager.findFragmentByTag(ALL_TAG);
369            mGroupsFragment = (GroupBrowseListFragment)
370                    fragmentManager.findFragmentByTag(GROUPS_TAG);
371
372            if (mFavoritesFragment == null) {
373                mFavoritesFragment = new ContactTileListFragment();
374                mAllFragment = new DefaultContactBrowseListFragment();
375                mGroupsFragment = new GroupBrowseListFragment();
376
377                transaction.add(R.id.tab_pager, mFavoritesFragment, FAVORITE_TAG);
378                transaction.add(R.id.tab_pager, mAllFragment, ALL_TAG);
379                transaction.add(R.id.tab_pager, mGroupsFragment, GROUPS_TAG);
380            }
381        }
382
383        mFavoritesFragment.setListener(mFavoritesFragmentListener);
384
385        mAllFragment.setOnContactListActionListener(new ContactBrowserActionListener());
386
387        mGroupsFragment.setListener(new GroupBrowserActionListener());
388
389        // Hide all fragments for now.  We adjust visibility when we get onSelectedTabChanged()
390        // from ActionBarAdapter.
391        transaction.hide(mFavoritesFragment);
392        transaction.hide(mAllFragment);
393        transaction.hide(mGroupsFragment);
394
395        if (isUsingTwoPanes) {
396            // Prepare 2-pane only fragments/views...
397
398            // Container views for fragments
399            mFavoritesView = getView(R.id.favorites_view);
400            mContactDetailsView = getView(R.id.contact_details_view);
401            mGroupDetailsView = getView(R.id.group_details_view);
402            mBrowserView = getView(R.id.browse_view);
403
404            // 2-pane only fragments
405            mFrequentFragment = getFragment(R.id.frequent_fragment);
406            mFrequentFragment.setListener(mFavoritesFragmentListener);
407            mFrequentFragment.setDisplayType(DisplayType.FREQUENT_ONLY);
408            mFrequentFragment.enableQuickContact(true);
409
410            mContactDetailLoaderFragment = getFragment(R.id.contact_detail_loader_fragment);
411            mContactDetailLoaderFragment.setListener(mContactDetailLoaderFragmentListener);
412
413            mGroupDetailFragment = getFragment(R.id.group_detail_fragment);
414            mGroupDetailFragment.setListener(mGroupDetailFragmentListener);
415            mGroupDetailFragment.setQuickContact(true);
416
417            if (mContactDetailFragment != null) {
418                transaction.hide(mContactDetailFragment);
419            }
420            transaction.hide(mGroupDetailFragment);
421
422            // Configure contact details
423            mContactDetailLayoutController = new ContactDetailLayoutController(this, savedState,
424                    getFragmentManager(), mContactDetailsView,
425                    findViewById(R.id.contact_detail_container),
426                    new ContactDetailFragmentListener());
427        }
428        transaction.commitAllowingStateLoss();
429        fragmentManager.executePendingTransactions();
430
431        // Setting Properties after fragment is created
432        if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
433            mFavoritesFragment.enableQuickContact(true);
434            mFavoritesFragment.setDisplayType(DisplayType.STARRED_ONLY);
435        } else {
436            mFavoritesFragment.setDisplayType(DisplayType.STREQUENT);
437        }
438
439        // Configure action bar
440        mActionBarAdapter = new ActionBarAdapter(this, this, getActionBar(), isUsingTwoPanes);
441        mActionBarAdapter.initialize(savedState, mRequest);
442
443        invalidateOptionsMenuIfNeeded();
444    }
445
446    @Override
447    protected void onStart() {
448        if (!mFragmentInitialized) {
449            mFragmentInitialized = true;
450            /* Configure fragments if we haven't.
451             *
452             * Note it's a one-shot initialization, so we want to do this in {@link #onCreate}.
453             *
454             * However, because this method may indirectly touch views in fragments but fragments
455             * created in {@link #configureContentView} using a {@link FragmentTransaction} will NOT
456             * have views until {@link Activity#onCreate} finishes (they would if they were inflated
457             * from a layout), we need to do it here in {@link #onStart()}.
458             *
459             * (When {@link Fragment#onCreateView} is called is different in the former case and
460             * in the latter case, unfortunately.)
461             *
462             * Also, we skip most of the work in it if the activity is a re-created one.
463             * (so the argument.)
464             */
465            configureFragments(!mIsRecreatedInstance);
466        } else if (PhoneCapabilityTester.isUsingTwoPanes(this) && !mCurrentFilterIsValid) {
467            // We only want to do the filter check in onStart for wide screen devices where it
468            // is often possible to get into single contact mode. Only do this check if
469            // the filter hasn't already been set properly (i.e. onCreate or onActivityResult).
470
471            // Since there is only one {@link ContactListFilterController} across multiple
472            // activity instances, make sure the filter controller is in sync withthe current
473            // contact list fragment filter.
474            // TODO: Clean this up. Perhaps change {@link ContactListFilterController} to not be a
475            // singleton?
476            mContactListFilterController.setContactListFilter(mAllFragment.getFilter(), true);
477            mContactListFilterController.checkFilterValidity(true);
478            mCurrentFilterIsValid = true;
479        }
480        super.onStart();
481    }
482
483    @Override
484    protected void onPause() {
485        mOptionsMenuContactsAvailable = false;
486
487        mProviderStatus = -1;
488        mProviderStatusLoader.setProviderStatusListener(null);
489        super.onPause();
490    }
491
492    @Override
493    protected void onResume() {
494        super.onResume();
495        mProviderStatusLoader.setProviderStatusListener(this);
496        showContactsUnavailableFragmentIfNecessary();
497
498        // Re-register the listener, which may have been cleared when onSaveInstanceState was
499        // called.  See also: onSaveInstanceState
500        mActionBarAdapter.setListener(this);
501        if (mTabPager != null) {
502            mTabPager.setOnPageChangeListener(mTabPagerListener);
503        }
504        // Current tab may have changed since the last onSaveInstanceState().  Make sure
505        // the actual contents match the tab.
506        updateFragmentsVisibility();
507    }
508
509    @Override
510    protected void onStop() {
511        super.onStop();
512        mCurrentFilterIsValid = false;
513    }
514
515    @Override
516    protected void onDestroy() {
517        // Some of variables will be null if this Activity redirects Intent.
518        // See also onCreate() or other methods called during the Activity's initialization.
519        if (mActionBarAdapter != null) {
520            mActionBarAdapter.setListener(null);
521        }
522        if (mContactListFilterController != null) {
523            mContactListFilterController.removeListener(this);
524        }
525
526        super.onDestroy();
527    }
528
529    private void configureFragments(boolean fromRequest) {
530        if (fromRequest) {
531            ContactListFilter filter = null;
532            int actionCode = mRequest.getActionCode();
533            boolean searchMode = mRequest.isSearchMode();
534            final int tabToOpen;
535            switch (actionCode) {
536                case ContactsRequest.ACTION_ALL_CONTACTS:
537                    filter = ContactListFilter.createFilterWithType(
538                            ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
539                    tabToOpen = TabState.ALL;
540                    break;
541                case ContactsRequest.ACTION_CONTACTS_WITH_PHONES:
542                    filter = ContactListFilter.createFilterWithType(
543                            ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY);
544                    tabToOpen = TabState.ALL;
545                    break;
546
547                case ContactsRequest.ACTION_FREQUENT:
548                case ContactsRequest.ACTION_STREQUENT:
549                case ContactsRequest.ACTION_STARRED:
550                    tabToOpen = TabState.FAVORITES;
551                    break;
552                case ContactsRequest.ACTION_VIEW_CONTACT:
553                    // We redirect this intent to the detail activity on 1-pane, so we don't get
554                    // here.  It's only for 2-pane.
555                    tabToOpen = TabState.ALL;
556                    break;
557                case ContactsRequest.ACTION_GROUP:
558                    tabToOpen = TabState.GROUPS;
559                    break;
560                default:
561                    tabToOpen = -1;
562                    break;
563            }
564            if (tabToOpen != -1) {
565                mActionBarAdapter.setCurrentTab(tabToOpen);
566            }
567
568            if (filter != null) {
569                mContactListFilterController.setContactListFilter(filter, false);
570                searchMode = false;
571            }
572
573            if (mRequest.getContactUri() != null) {
574                searchMode = false;
575            }
576
577            mActionBarAdapter.setSearchMode(searchMode);
578            configureContactListFragmentForRequest();
579        }
580
581        configureContactListFragment();
582        configureGroupListFragment();
583
584        invalidateOptionsMenuIfNeeded();
585    }
586
587    @Override
588    public void onContactListFilterChanged() {
589        if (mAllFragment == null || !mAllFragment.isAdded()) {
590            return;
591        }
592
593        mAllFragment.setFilter(mContactListFilterController.getFilter());
594
595        invalidateOptionsMenuIfNeeded();
596    }
597
598    private void setupContactDetailFragment(final Uri contactLookupUri) {
599        mContactDetailLoaderFragment.loadUri(contactLookupUri);
600        invalidateOptionsMenuIfNeeded();
601    }
602
603    private void setupGroupDetailFragment(Uri groupUri) {
604        // If we are switching from one group to another, do a cross-fade
605        if (mGroupDetailFragment != null && mGroupDetailFragment.getGroupUri() != null &&
606                !UriUtils.areEqual(mGroupDetailFragment.getGroupUri(), groupUri)) {
607            mGroupDetailsView.startTransition(mGroupDetailFragment.getView(), false);
608        }
609        mGroupDetailFragment.loadGroup(groupUri);
610        invalidateOptionsMenuIfNeeded();
611    }
612
613    /**
614     * Handler for action bar actions.
615     */
616    @Override
617    public void onAction(Action action) {
618        switch (action) {
619            case START_SEARCH_MODE:
620                // Tell the fragments that we're in the search mode
621                configureFragments(false /* from request */);
622                updateFragmentsVisibility();
623                invalidateOptionsMenu();
624                break;
625            case STOP_SEARCH_MODE:
626                setQueryTextToFragment("");
627                updateFragmentsVisibility();
628                invalidateOptionsMenu();
629                break;
630            case CHANGE_SEARCH_QUERY:
631                setQueryTextToFragment(mActionBarAdapter.getQueryString());
632                break;
633            default:
634                throw new IllegalStateException("Unkonwn ActionBarAdapter action: " + action);
635        }
636    }
637
638    @Override
639    public void onSelectedTabChanged() {
640        updateFragmentsVisibility();
641    }
642
643    /**
644     * Updates the fragment/view visibility according to the current mode, such as
645     * {@link ActionBarAdapter#isSearchMode()} and {@link ActionBarAdapter#getCurrentTab()}.
646     */
647    private void updateFragmentsVisibility() {
648        int tab = mActionBarAdapter.getCurrentTab();
649
650        // We use ViewPager on 1-pane.
651        if (!PhoneCapabilityTester.isUsingTwoPanes(this)) {
652            if (mActionBarAdapter.isSearchMode()) {
653                mTabPagerAdapter.setSearchMode(true);
654            } else {
655                // No smooth scrolling if quitting from the search mode.
656                final boolean wasSearchMode = mTabPagerAdapter.isSearchMode();
657                mTabPagerAdapter.setSearchMode(false);
658                if (mTabPager.getCurrentItem() != tab) {
659                    mTabPager.setCurrentItem(tab, !wasSearchMode);
660                }
661            }
662            invalidateOptionsMenu();
663            showEmptyStateForTab(tab);
664            if (tab == TabState.GROUPS) {
665                mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable());
666            }
667            return;
668        }
669
670        // for the tablet...
671
672        // If in search mode, we use the all list + contact details to show the result.
673        if (mActionBarAdapter.isSearchMode()) {
674            tab = TabState.ALL;
675        }
676        switch (tab) {
677            case TabState.FAVORITES:
678                mFavoritesView.setVisibility(View.VISIBLE);
679                mBrowserView.setVisibility(View.GONE);
680                mGroupDetailsView.setVisibility(View.GONE);
681                mContactDetailsView.setVisibility(View.GONE);
682                break;
683            case TabState.GROUPS:
684                mFavoritesView.setVisibility(View.GONE);
685                mBrowserView.setVisibility(View.VISIBLE);
686                mGroupDetailsView.setVisibility(View.VISIBLE);
687                mContactDetailsView.setVisibility(View.GONE);
688                mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable());
689                break;
690            case TabState.ALL:
691                mFavoritesView.setVisibility(View.GONE);
692                mBrowserView.setVisibility(View.VISIBLE);
693                mContactDetailsView.setVisibility(View.VISIBLE);
694                mGroupDetailsView.setVisibility(View.GONE);
695                break;
696        }
697        FragmentManager fragmentManager = getFragmentManager();
698        FragmentTransaction ft = fragmentManager.beginTransaction();
699
700        // Note mContactDetailLoaderFragment is an invisible fragment, but we still have to show/
701        // hide it so its options menu will be shown/hidden.
702        switch (tab) {
703            case TabState.FAVORITES:
704                showFragment(ft, mFavoritesFragment);
705                showFragment(ft, mFrequentFragment);
706                hideFragment(ft, mAllFragment);
707                hideFragment(ft, mContactDetailLoaderFragment);
708                hideFragment(ft, mContactDetailFragment);
709                hideFragment(ft, mGroupsFragment);
710                hideFragment(ft, mGroupDetailFragment);
711                break;
712            case TabState.ALL:
713                hideFragment(ft, mFavoritesFragment);
714                hideFragment(ft, mFrequentFragment);
715                showFragment(ft, mAllFragment);
716                showFragment(ft, mContactDetailLoaderFragment);
717                showFragment(ft, mContactDetailFragment);
718                hideFragment(ft, mGroupsFragment);
719                hideFragment(ft, mGroupDetailFragment);
720                break;
721            case TabState.GROUPS:
722                hideFragment(ft, mFavoritesFragment);
723                hideFragment(ft, mFrequentFragment);
724                hideFragment(ft, mAllFragment);
725                hideFragment(ft, mContactDetailLoaderFragment);
726                hideFragment(ft, mContactDetailFragment);
727                showFragment(ft, mGroupsFragment);
728                showFragment(ft, mGroupDetailFragment);
729                break;
730        }
731        if (!ft.isEmpty()) {
732            ft.commitAllowingStateLoss();
733            fragmentManager.executePendingTransactions();
734            // When switching tabs, we need to invalidate options menu, but executing a
735            // fragment transaction does it implicitly.  We don't have to call invalidateOptionsMenu
736            // manually.
737        }
738        showEmptyStateForTab(tab);
739    }
740
741    private void showEmptyStateForTab(int tab) {
742        if (mContactsUnavailableFragment != null) {
743            switch (tab) {
744                case TabState.FAVORITES:
745                    mContactsUnavailableFragment.setMessageText(
746                            R.string.listTotalAllContactsZeroStarred, -1);
747                    break;
748                case TabState.GROUPS:
749                    mContactsUnavailableFragment.setMessageText(R.string.noGroups,
750                            areGroupWritableAccountsAvailable() ? -1 : R.string.noAccounts);
751                    break;
752                case TabState.ALL:
753                    mContactsUnavailableFragment.setMessageText(R.string.noContacts, -1);
754                    break;
755            }
756        }
757    }
758
759    private class TabPagerListener implements ViewPager.OnPageChangeListener {
760        @Override
761        public void onPageScrollStateChanged(int state) {
762        }
763
764        @Override
765        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
766        }
767
768        @Override
769        public void onPageSelected(int position) {
770            // Make sure not in the search mode, in which case position != TabState.ordinal().
771            if (!mTabPagerAdapter.isSearchMode()) {
772                mActionBarAdapter.setCurrentTab(position, false);
773                showEmptyStateForTab(position);
774                if (position == TabState.GROUPS) {
775                    mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable());
776                }
777                invalidateOptionsMenu();
778            }
779        }
780    }
781
782    /**
783     * Adapter for the {@link ViewPager}.  Unlike {@link FragmentPagerAdapter},
784     * {@link #instantiateItem} returns existing fragments, and {@link #instantiateItem}/
785     * {@link #destroyItem} show/hide fragments instead of attaching/detaching.
786     *
787     * In search mode, we always show the "all" fragment, and disable the swipe.  We change the
788     * number of items to 1 to disable the swipe.
789     *
790     * TODO figure out a more straight way to disable swipe.
791     */
792    private class TabPagerAdapter extends PagerAdapter {
793        private final FragmentManager mFragmentManager;
794        private FragmentTransaction mCurTransaction = null;
795
796        private boolean mTabPagerAdapterSearchMode;
797
798        private Fragment mCurrentPrimaryItem;
799
800        public TabPagerAdapter() {
801            mFragmentManager = getFragmentManager();
802        }
803
804        public boolean isSearchMode() {
805            return mTabPagerAdapterSearchMode;
806        }
807
808        public void setSearchMode(boolean searchMode) {
809            if (searchMode == mTabPagerAdapterSearchMode) {
810                return;
811            }
812            mTabPagerAdapterSearchMode = searchMode;
813            notifyDataSetChanged();
814        }
815
816        @Override
817        public int getCount() {
818            return mTabPagerAdapterSearchMode ? 1 : TabState.COUNT;
819        }
820
821        /** Gets called when the number of items changes. */
822        @Override
823        public int getItemPosition(Object object) {
824            if (mTabPagerAdapterSearchMode) {
825                if (object == mAllFragment) {
826                    return 0; // Only 1 page in search mode
827                }
828            } else {
829                if (object == mFavoritesFragment) {
830                    return TabState.FAVORITES;
831                }
832                if (object == mAllFragment) {
833                    return TabState.ALL;
834                }
835                if (object == mGroupsFragment) {
836                    return TabState.GROUPS;
837                }
838            }
839            return POSITION_NONE;
840        }
841
842        @Override
843        public void startUpdate(ViewGroup container) {
844        }
845
846        private Fragment getFragment(int position) {
847            if (mTabPagerAdapterSearchMode) {
848                if (position == 0) {
849                    return mAllFragment;
850                }
851            } else {
852                if (position == TabState.FAVORITES) {
853                    return mFavoritesFragment;
854                } else if (position == TabState.ALL) {
855                    return mAllFragment;
856                } else if (position == TabState.GROUPS) {
857                    return mGroupsFragment;
858                }
859            }
860            throw new IllegalArgumentException("position: " + position);
861        }
862
863        @Override
864        public Object instantiateItem(ViewGroup container, int position) {
865            if (mCurTransaction == null) {
866                mCurTransaction = mFragmentManager.beginTransaction();
867            }
868            Fragment f = getFragment(position);
869            mCurTransaction.show(f);
870
871            // Non primary pages are not visible.
872            f.setUserVisibleHint(f == mCurrentPrimaryItem);
873            return f;
874        }
875
876        @Override
877        public void destroyItem(ViewGroup container, int position, Object object) {
878            if (mCurTransaction == null) {
879                mCurTransaction = mFragmentManager.beginTransaction();
880            }
881            mCurTransaction.hide((Fragment) object);
882        }
883
884        @Override
885        public void finishUpdate(ViewGroup container) {
886            if (mCurTransaction != null) {
887                mCurTransaction.commitAllowingStateLoss();
888                mCurTransaction = null;
889                mFragmentManager.executePendingTransactions();
890            }
891        }
892
893        @Override
894        public boolean isViewFromObject(View view, Object object) {
895            return ((Fragment) object).getView() == view;
896        }
897
898        @Override
899        public void setPrimaryItem(ViewGroup container, int position, Object object) {
900            Fragment fragment = (Fragment) object;
901            if (mCurrentPrimaryItem != fragment) {
902                if (mCurrentPrimaryItem != null) {
903                    mCurrentPrimaryItem.setUserVisibleHint(false);
904                }
905                if (fragment != null) {
906                    fragment.setUserVisibleHint(true);
907                }
908                mCurrentPrimaryItem = fragment;
909            }
910        }
911
912        @Override
913        public Parcelable saveState() {
914            return null;
915        }
916
917        @Override
918        public void restoreState(Parcelable state, ClassLoader loader) {
919        }
920    }
921
922    private void setQueryTextToFragment(String query) {
923        mAllFragment.setQueryString(query, true);
924        mAllFragment.setVisibleScrollbarEnabled(!mAllFragment.isSearchMode());
925    }
926
927    private void configureContactListFragmentForRequest() {
928        Uri contactUri = mRequest.getContactUri();
929        if (contactUri != null) {
930            // For an incoming request, explicitly require a selection if we are on 2-pane UI,
931            // (i.e. even if we view the same selected contact, the contact may no longer be
932            // in the list, so we must refresh the list).
933            if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
934                mAllFragment.setSelectionRequired(true);
935            }
936            mAllFragment.setSelectedContactUri(contactUri);
937        }
938
939        mAllFragment.setFilter(mContactListFilterController.getFilter());
940        setQueryTextToFragment(mActionBarAdapter.getQueryString());
941
942        if (mRequest.isDirectorySearchEnabled()) {
943            mAllFragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_DEFAULT);
944        } else {
945            mAllFragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE);
946        }
947    }
948
949    private void configureContactListFragment() {
950        // Filter may be changed when this Activity is in background.
951        mAllFragment.setFilter(mContactListFilterController.getFilter());
952
953        final boolean useTwoPane = PhoneCapabilityTester.isUsingTwoPanes(this);
954        mAllFragment.setVerticalScrollbarPosition(
955                useTwoPane
956                        ? View.SCROLLBAR_POSITION_LEFT
957                        : View.SCROLLBAR_POSITION_RIGHT);
958        mAllFragment.setSelectionVisible(useTwoPane);
959        mAllFragment.setQuickContactEnabled(!useTwoPane);
960    }
961
962    private void configureGroupListFragment() {
963        final boolean useTwoPane = PhoneCapabilityTester.isUsingTwoPanes(this);
964        mGroupsFragment.setVerticalScrollbarPosition(
965                useTwoPane
966                        ? View.SCROLLBAR_POSITION_LEFT
967                        : View.SCROLLBAR_POSITION_RIGHT);
968        mGroupsFragment.setSelectionVisible(useTwoPane);
969    }
970
971    @Override
972    public void onProviderStatusChange() {
973        showContactsUnavailableFragmentIfNecessary();
974    }
975
976    private void showContactsUnavailableFragmentIfNecessary() {
977        int providerStatus = mProviderStatusLoader.getProviderStatus();
978        if (providerStatus == mProviderStatus) {
979            return;
980        }
981
982        mProviderStatus = providerStatus;
983
984        View contactsUnavailableView = findViewById(R.id.contacts_unavailable_view);
985        View mainView = findViewById(R.id.main_view);
986
987        if (mProviderStatus == ProviderStatus.STATUS_NORMAL) {
988            // Ensure that the mTabPager is visible; we may have made it invisible below.
989            contactsUnavailableView.setVisibility(View.GONE);
990            if (mTabPager != null) {
991                mTabPager.setVisibility(View.VISIBLE);
992            }
993
994            if (mainView != null) {
995                mainView.setVisibility(View.VISIBLE);
996            }
997            if (mAllFragment != null) {
998                mAllFragment.setEnabled(true);
999            }
1000        } else {
1001            // If there are no accounts on the device and we should show the "no account" prompt
1002            // (based on {@link SharedPreferences}), then launch the account setup activity so the
1003            // user can sign-in or create an account.
1004            if (!areContactWritableAccountsAvailable() &&
1005                    AccountPromptUtils.shouldShowAccountPrompt(this)) {
1006                AccountPromptUtils.launchAccountPrompt(this);
1007                return;
1008            }
1009
1010            // Otherwise, continue setting up the page so that the user can still use the app
1011            // without an account.
1012            if (mAllFragment != null) {
1013                mAllFragment.setEnabled(false);
1014            }
1015            if (mContactsUnavailableFragment == null) {
1016                mContactsUnavailableFragment = new ContactsUnavailableFragment();
1017                mContactsUnavailableFragment.setProviderStatusLoader(mProviderStatusLoader);
1018                mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
1019                        new ContactsUnavailableFragmentListener());
1020                getFragmentManager().beginTransaction()
1021                        .replace(R.id.contacts_unavailable_container, mContactsUnavailableFragment)
1022                        .commitAllowingStateLoss();
1023            } else {
1024                mContactsUnavailableFragment.update();
1025            }
1026
1027            // Show the contactsUnavailableView, and hide the mTabPager so that we don't
1028            // see it sliding in underneath the contactsUnavailableView at the edges.
1029            contactsUnavailableView.setVisibility(View.VISIBLE);
1030            if (mTabPager != null) {
1031                mTabPager.setVisibility(View.GONE);
1032            }
1033
1034            if (mainView != null) {
1035                mainView.setVisibility(View.INVISIBLE);
1036            }
1037
1038            showEmptyStateForTab(mActionBarAdapter.getCurrentTab());
1039        }
1040
1041        invalidateOptionsMenuIfNeeded();
1042    }
1043
1044    private final class ContactBrowserActionListener implements OnContactBrowserActionListener {
1045
1046        @Override
1047        public void onSelectionChange() {
1048            if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) {
1049                setupContactDetailFragment(mAllFragment.getSelectedContactUri());
1050            }
1051        }
1052
1053        @Override
1054        public void onViewContactAction(Uri contactLookupUri) {
1055            if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) {
1056                setupContactDetailFragment(contactLookupUri);
1057            } else {
1058                Intent intent = new Intent(Intent.ACTION_VIEW, contactLookupUri);
1059                // In search mode, the "up" affordance in the contact detail page should return the
1060                // user to the search results, so finish the activity when that button is selected.
1061                if (mActionBarAdapter.isSearchMode()) {
1062                    intent.putExtra(
1063                            ContactDetailActivity.INTENT_KEY_FINISH_ACTIVITY_ON_UP_SELECTED, true);
1064                }
1065                startActivity(intent);
1066            }
1067        }
1068
1069        @Override
1070        public void onCreateNewContactAction() {
1071            Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
1072            Bundle extras = getIntent().getExtras();
1073            if (extras != null) {
1074                intent.putExtras(extras);
1075            }
1076            startActivity(intent);
1077        }
1078
1079        @Override
1080        public void onEditContactAction(Uri contactLookupUri) {
1081            Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri);
1082            Bundle extras = getIntent().getExtras();
1083            if (extras != null) {
1084                intent.putExtras(extras);
1085            }
1086            intent.putExtra(
1087                    ContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, true);
1088            startActivityForResult(intent, SUBACTIVITY_EDIT_CONTACT);
1089        }
1090
1091        @Override
1092        public void onAddToFavoritesAction(Uri contactUri) {
1093            ContentValues values = new ContentValues(1);
1094            values.put(Contacts.STARRED, 1);
1095            getContentResolver().update(contactUri, values, null, null);
1096        }
1097
1098        @Override
1099        public void onRemoveFromFavoritesAction(Uri contactUri) {
1100            ContentValues values = new ContentValues(1);
1101            values.put(Contacts.STARRED, 0);
1102            getContentResolver().update(contactUri, values, null, null);
1103        }
1104
1105        @Override
1106        public void onCallContactAction(Uri contactUri) {
1107            PhoneNumberInteraction.startInteractionForPhoneCall(PeopleActivity.this, contactUri);
1108        }
1109
1110        @Override
1111        public void onSmsContactAction(Uri contactUri) {
1112            PhoneNumberInteraction.startInteractionForTextMessage(PeopleActivity.this, contactUri);
1113        }
1114
1115        @Override
1116        public void onDeleteContactAction(Uri contactUri) {
1117            ContactDeletionInteraction.start(PeopleActivity.this, contactUri, false);
1118        }
1119
1120        @Override
1121        public void onFinishAction() {
1122            onBackPressed();
1123        }
1124
1125        @Override
1126        public void onInvalidSelection() {
1127            ContactListFilter filter;
1128            ContactListFilter currentFilter = mAllFragment.getFilter();
1129            if (currentFilter != null
1130                    && currentFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
1131                filter = ContactListFilter.createFilterWithType(
1132                        ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
1133                mAllFragment.setFilter(filter);
1134            } else {
1135                filter = ContactListFilter.createFilterWithType(
1136                        ContactListFilter.FILTER_TYPE_SINGLE_CONTACT);
1137                mAllFragment.setFilter(filter, false);
1138            }
1139            mContactListFilterController.setContactListFilter(filter, true);
1140        }
1141    }
1142
1143    private class ContactDetailLoaderFragmentListener implements ContactLoaderFragmentListener {
1144        @Override
1145        public void onContactNotFound() {
1146            // Nothing needs to be done here
1147        }
1148
1149        @Override
1150        public void onDetailsLoaded(final ContactLoader.Result result) {
1151            if (result == null) {
1152                // Nothing is loaded. Show empty state.
1153                mContactDetailLayoutController.showEmptyState();
1154                return;
1155            }
1156            // Since {@link FragmentTransaction}s cannot be done in the onLoadFinished() of the
1157            // {@link LoaderCallbacks}, then post this {@link Runnable} to the {@link Handler}
1158            // on the main thread to execute later.
1159            mHandler.post(new Runnable() {
1160                @Override
1161                public void run() {
1162                    // If the activity is destroyed (or will be destroyed soon), don't update the UI
1163                    if (isFinishing()) {
1164                        return;
1165                    }
1166                    mContactDetailLayoutController.setContactData(result);
1167                }
1168            });
1169        }
1170
1171        @Override
1172        public void onEditRequested(Uri contactLookupUri) {
1173            Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri);
1174            intent.putExtra(
1175                    ContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, true);
1176            startActivityForResult(intent, SUBACTIVITY_EDIT_CONTACT);
1177        }
1178
1179        @Override
1180        public void onDeleteRequested(Uri contactUri) {
1181            ContactDeletionInteraction.start(PeopleActivity.this, contactUri, false);
1182        }
1183    }
1184
1185    public class ContactDetailFragmentListener implements ContactDetailFragment.Listener {
1186        @Override
1187        public void onItemClicked(Intent intent) {
1188            if (intent == null) {
1189                return;
1190            }
1191            try {
1192                startActivity(intent);
1193            } catch (ActivityNotFoundException e) {
1194                Log.e(TAG, "No activity found for intent: " + intent);
1195            }
1196        }
1197
1198        @Override
1199        public void onCreateRawContactRequested(ArrayList<ContentValues> values,
1200                AccountWithDataSet account) {
1201            Toast.makeText(PeopleActivity.this, R.string.toast_making_personal_copy,
1202                    Toast.LENGTH_LONG).show();
1203            Intent serviceIntent = ContactSaveService.createNewRawContactIntent(
1204                    PeopleActivity.this, values, account,
1205                    PeopleActivity.class, Intent.ACTION_VIEW);
1206            startService(serviceIntent);
1207        }
1208    }
1209
1210    private class ContactsUnavailableFragmentListener
1211            implements OnContactsUnavailableActionListener {
1212
1213        @Override
1214        public void onCreateNewContactAction() {
1215            startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
1216        }
1217
1218        @Override
1219        public void onAddAccountAction() {
1220            Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
1221            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
1222            intent.putExtra(Settings.EXTRA_AUTHORITIES,
1223                    new String[] { ContactsContract.AUTHORITY });
1224            startActivity(intent);
1225        }
1226
1227        @Override
1228        public void onImportContactsFromFileAction() {
1229            ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable());
1230        }
1231
1232        @Override
1233        public void onFreeInternalStorageAction() {
1234            startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
1235        }
1236    }
1237
1238    private final class StrequentContactListFragmentListener
1239            implements ContactTileListFragment.Listener {
1240        @Override
1241        public void onContactSelected(Uri contactUri, Rect targetRect) {
1242            if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) {
1243                QuickContact.showQuickContact(PeopleActivity.this, targetRect, contactUri, 0, null);
1244            } else {
1245                startActivity(new Intent(Intent.ACTION_VIEW, contactUri));
1246            }
1247        }
1248
1249        @Override
1250        public void onCallNumberDirectly(String phoneNumber) {
1251            // No need to call phone number directly from People app.
1252            Log.w(TAG, "unexpected invocation of onCallNumberDirectly()");
1253        }
1254    }
1255
1256    private final class GroupBrowserActionListener implements OnGroupBrowserActionListener {
1257
1258        @Override
1259        public void onViewGroupAction(Uri groupUri) {
1260            if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) {
1261                setupGroupDetailFragment(groupUri);
1262            } else {
1263                Intent intent = new Intent(PeopleActivity.this, GroupDetailActivity.class);
1264                intent.setData(groupUri);
1265                startActivity(intent);
1266            }
1267        }
1268    }
1269
1270    private class GroupDetailFragmentListener implements GroupDetailFragment.Listener {
1271        @Override
1272        public void onGroupSizeUpdated(String size) {
1273            // Nothing needs to be done here because the size will be displayed in the detail
1274            // fragment
1275        }
1276
1277        @Override
1278        public void onGroupTitleUpdated(String title) {
1279            // Nothing needs to be done here because the title will be displayed in the detail
1280            // fragment
1281        }
1282
1283        @Override
1284        public void onAccountTypeUpdated(String accountTypeString, String dataSet) {
1285            // Nothing needs to be done here because the group source will be displayed in the
1286            // detail fragment
1287        }
1288
1289        @Override
1290        public void onEditRequested(Uri groupUri) {
1291            final Intent intent = new Intent(PeopleActivity.this, GroupEditorActivity.class);
1292            intent.setData(groupUri);
1293            intent.setAction(Intent.ACTION_EDIT);
1294            startActivityForResult(intent, SUBACTIVITY_EDIT_GROUP);
1295        }
1296
1297        @Override
1298        public void onContactSelected(Uri contactUri) {
1299            // Nothing needs to be done here because either quickcontact will be displayed
1300            // or activity will take care of selection
1301        }
1302    }
1303
1304    public void startActivityAndForwardResult(final Intent intent) {
1305        intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
1306
1307        // Forward extras to the new activity
1308        Bundle extras = getIntent().getExtras();
1309        if (extras != null) {
1310            intent.putExtras(extras);
1311        }
1312        startActivity(intent);
1313        finish();
1314    }
1315
1316    @Override
1317    public boolean onCreateOptionsMenu(Menu menu) {
1318        if (!areContactsAvailable()) {
1319            // If contacts aren't available, hide all menu items.
1320            return false;
1321        }
1322        super.onCreateOptionsMenu(menu);
1323
1324        MenuInflater inflater = getMenuInflater();
1325        inflater.inflate(R.menu.people_options, menu);
1326
1327        // On narrow screens we specify a NEW group button in the {@link ActionBar}, so that
1328        // it can be in the overflow menu. On wide screens, we use a custom view because we need
1329        // its location for anchoring the account-selector popup.
1330        final MenuItem addGroup = menu.findItem(R.id.menu_custom_add_group);
1331        if (addGroup != null) {
1332            mAddGroupImageView = getLayoutInflater().inflate(
1333                    R.layout.add_group_menu_item, null, false);
1334            mAddGroupImageView.setOnClickListener(new OnClickListener() {
1335                @Override
1336                public void onClick(View v) {
1337                    createNewGroupWithAccountDisambiguation();
1338                }
1339            });
1340            addGroup.setActionView(mAddGroupImageView);
1341        }
1342
1343        if (DEBUG_TRANSITIONS && mContactDetailLoaderFragment != null) {
1344            final MenuItem toggleSocial =
1345                    menu.add(mContactDetailLoaderFragment.getLoadStreamItems() ? "less" : "more");
1346            toggleSocial.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1347            toggleSocial.setOnMenuItemClickListener(new OnMenuItemClickListener() {
1348                @Override
1349                public boolean onMenuItemClick(MenuItem item) {
1350                    mContactDetailLoaderFragment.toggleLoadStreamItems();
1351                    invalidateOptionsMenu();
1352                    return false;
1353                }
1354            });
1355        }
1356
1357        return true;
1358    }
1359
1360    private void invalidateOptionsMenuIfNeeded() {
1361        if (isOptionsMenuChanged()) {
1362            invalidateOptionsMenu();
1363        }
1364    }
1365
1366    public boolean isOptionsMenuChanged() {
1367        if (mOptionsMenuContactsAvailable != areContactsAvailable()) {
1368            return true;
1369        }
1370
1371        if (mAllFragment != null && mAllFragment.isOptionsMenuChanged()) {
1372            return true;
1373        }
1374
1375        if (mContactDetailLoaderFragment != null &&
1376                mContactDetailLoaderFragment.isOptionsMenuChanged()) {
1377            return true;
1378        }
1379
1380        if (mGroupDetailFragment != null && mGroupDetailFragment.isOptionsMenuChanged()) {
1381            return true;
1382        }
1383
1384        return false;
1385    }
1386
1387    @Override
1388    public boolean onPrepareOptionsMenu(Menu menu) {
1389        mOptionsMenuContactsAvailable = areContactsAvailable();
1390        if (!mOptionsMenuContactsAvailable) {
1391            return false;
1392        }
1393
1394        // Get references to individual menu items in the menu
1395        final MenuItem addContactMenu = menu.findItem(R.id.menu_add_contact);
1396        final MenuItem contactsFilterMenu = menu.findItem(R.id.menu_contacts_filter);
1397
1398        MenuItem addGroupMenu = menu.findItem(R.id.menu_add_group);
1399        if (addGroupMenu == null) {
1400            addGroupMenu = menu.findItem(R.id.menu_custom_add_group);
1401        }
1402
1403        final MenuItem clearFrequentsMenu = menu.findItem(R.id.menu_clear_frequents);
1404
1405        final boolean isSearchMode = mActionBarAdapter.isSearchMode();
1406        if (isSearchMode) {
1407            addContactMenu.setVisible(false);
1408            addGroupMenu.setVisible(false);
1409            contactsFilterMenu.setVisible(false);
1410            clearFrequentsMenu.setVisible(false);
1411        } else {
1412            switch (mActionBarAdapter.getCurrentTab()) {
1413                case TabState.FAVORITES:
1414                    addContactMenu.setVisible(false);
1415                    addGroupMenu.setVisible(false);
1416                    contactsFilterMenu.setVisible(false);
1417                    clearFrequentsMenu.setVisible(hasFrequents());
1418                    break;
1419                case TabState.ALL:
1420                    addContactMenu.setVisible(true);
1421                    addGroupMenu.setVisible(false);
1422                    contactsFilterMenu.setVisible(true);
1423                    clearFrequentsMenu.setVisible(false);
1424                    break;
1425                case TabState.GROUPS:
1426                    // Do not display the "new group" button if no accounts are available
1427                    if (areGroupWritableAccountsAvailable()) {
1428                        addGroupMenu.setVisible(true);
1429                    } else {
1430                        addGroupMenu.setVisible(false);
1431                    }
1432                    addContactMenu.setVisible(false);
1433                    contactsFilterMenu.setVisible(false);
1434                    clearFrequentsMenu.setVisible(false);
1435                    break;
1436            }
1437        }
1438        final boolean showMiscOptions = !isSearchMode;
1439        makeMenuItemVisible(menu, R.id.menu_search, showMiscOptions);
1440        makeMenuItemVisible(menu, R.id.menu_import_export, showMiscOptions);
1441        makeMenuItemVisible(menu, R.id.menu_accounts, showMiscOptions);
1442        makeMenuItemVisible(menu, R.id.menu_settings,
1443                showMiscOptions && !ContactsPreferenceActivity.isEmpty(this));
1444
1445        return true;
1446    }
1447
1448    /**
1449     * Returns whether there are any frequently contacted people being displayed
1450     * @return
1451     */
1452    private boolean hasFrequents() {
1453        if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
1454            return mFrequentFragment.hasFrequents();
1455        } else {
1456            return mFavoritesFragment.hasFrequents();
1457        }
1458    }
1459
1460    private void makeMenuItemVisible(Menu menu, int itemId, boolean visible) {
1461        MenuItem item =menu.findItem(itemId);
1462        if (item != null) {
1463            item.setVisible(visible);
1464        }
1465    }
1466
1467    @Override
1468    public boolean onOptionsItemSelected(MenuItem item) {
1469        switch (item.getItemId()) {
1470            case android.R.id.home: {
1471                // The home icon on the action bar is pressed
1472                if (mActionBarAdapter.isUpShowing()) {
1473                    // "UP" icon press -- should be treated as "back".
1474                    onBackPressed();
1475                }
1476                return true;
1477            }
1478            case R.id.menu_settings: {
1479                final Intent intent = new Intent(this, ContactsPreferenceActivity.class);
1480                // as there is only one section right now, make sure it is selected
1481                // on small screens, this also hides the section selector
1482                // Due to b/5045558, this code unfortunately only works properly on phones
1483                boolean settingsAreMultiPane = getResources().getBoolean(
1484                        com.android.internal.R.bool.preferences_prefer_dual_pane);
1485                if (!settingsAreMultiPane) {
1486                    intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT,
1487                            DisplayOptionsPreferenceFragment.class.getName());
1488                    intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE,
1489                            R.string.preference_displayOptions);
1490                }
1491                startActivity(intent);
1492                return true;
1493            }
1494            case R.id.menu_contacts_filter: {
1495                AccountFilterUtil.startAccountFilterActivityForResult(
1496                        this, SUBACTIVITY_ACCOUNT_FILTER,
1497                        mContactListFilterController.getFilter());
1498                return true;
1499            }
1500            case R.id.menu_search: {
1501                onSearchRequested();
1502                return true;
1503            }
1504            case R.id.menu_add_contact: {
1505                final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
1506                // On 2-pane UI, we can let the editor activity finish itself and return
1507                // to this activity to display the new contact.
1508                if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
1509                    intent.putExtra(
1510                        ContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, true);
1511                    startActivityForResult(intent, SUBACTIVITY_NEW_CONTACT);
1512                } else {
1513                    // Otherwise, on 1-pane UI, we need the editor to launch the view contact
1514                    // intent itself.
1515                    startActivity(intent);
1516                }
1517                return true;
1518            }
1519            case R.id.menu_add_group: {
1520                createNewGroupWithAccountDisambiguation();
1521                return true;
1522            }
1523            case R.id.menu_import_export: {
1524                ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable());
1525                return true;
1526            }
1527            case R.id.menu_clear_frequents: {
1528                ClearFrequentsDialog.show(getFragmentManager());
1529                return true;
1530            }
1531            case R.id.menu_accounts: {
1532                final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
1533                intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] {
1534                    ContactsContract.AUTHORITY
1535                });
1536                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
1537                startActivity(intent);
1538                return true;
1539            }
1540        }
1541        return false;
1542    }
1543
1544    private void createNewGroupWithAccountDisambiguation() {
1545        final List<AccountWithDataSet> accounts =
1546                AccountTypeManager.getInstance(this).getAccounts(true);
1547        if (accounts.size() <= 1 || mAddGroupImageView == null) {
1548            // No account to choose or no control to anchor the popup-menu to
1549            // ==> just go straight to the editor which will disambig if necessary
1550            final Intent intent = new Intent(this, GroupEditorActivity.class);
1551            intent.setAction(Intent.ACTION_INSERT);
1552            startActivityForResult(intent, SUBACTIVITY_NEW_GROUP);
1553            return;
1554        }
1555
1556        final ListPopupWindow popup = new ListPopupWindow(this, null);
1557        popup.setWidth(getResources().getDimensionPixelSize(R.dimen.account_selector_popup_width));
1558        popup.setAnchorView(mAddGroupImageView);
1559        // Create a list adapter with all writeable accounts (assume that the writeable accounts all
1560        // allow group creation).
1561        final AccountsListAdapter adapter = new AccountsListAdapter(this,
1562                AccountListFilter.ACCOUNTS_GROUP_WRITABLE);
1563        popup.setAdapter(adapter);
1564        popup.setOnItemClickListener(new OnItemClickListener() {
1565            @Override
1566            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1567                popup.dismiss();
1568                AccountWithDataSet account = adapter.getItem(position);
1569                final Intent intent = new Intent(PeopleActivity.this, GroupEditorActivity.class);
1570                intent.setAction(Intent.ACTION_INSERT);
1571                intent.putExtra(Intents.Insert.ACCOUNT, account);
1572                intent.putExtra(Intents.Insert.DATA_SET, account.dataSet);
1573                startActivityForResult(intent, SUBACTIVITY_NEW_GROUP);
1574            }
1575        });
1576        popup.setModal(true);
1577        popup.show();
1578    }
1579
1580    @Override
1581    public boolean onSearchRequested() { // Search key pressed.
1582        mActionBarAdapter.setSearchMode(true);
1583        return true;
1584    }
1585
1586    @Override
1587    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1588        switch (requestCode) {
1589            case SUBACTIVITY_ACCOUNT_FILTER: {
1590                AccountFilterUtil.handleAccountFilterResult(
1591                        mContactListFilterController, resultCode, data);
1592                break;
1593            }
1594
1595            case SUBACTIVITY_NEW_CONTACT:
1596            case SUBACTIVITY_EDIT_CONTACT: {
1597                if (resultCode == RESULT_OK && PhoneCapabilityTester.isUsingTwoPanes(this)) {
1598                    mRequest.setActionCode(ContactsRequest.ACTION_VIEW_CONTACT);
1599                    mAllFragment.setSelectionRequired(true);
1600                    mAllFragment.setSelectedContactUri(data.getData());
1601                    // Suppress IME if in search mode
1602                    if (mActionBarAdapter != null) {
1603                        mActionBarAdapter.clearFocusOnSearchView();
1604                    }
1605                    // No need to change the contact filter
1606                    mCurrentFilterIsValid = true;
1607                }
1608                break;
1609            }
1610
1611            case SUBACTIVITY_NEW_GROUP:
1612            case SUBACTIVITY_EDIT_GROUP: {
1613                if (resultCode == RESULT_OK && PhoneCapabilityTester.isUsingTwoPanes(this)) {
1614                    mRequest.setActionCode(ContactsRequest.ACTION_GROUP);
1615                    mGroupsFragment.setSelectedUri(data.getData());
1616                }
1617                break;
1618            }
1619
1620            // TODO: Using the new startActivityWithResultFromFragment API this should not be needed
1621            // anymore
1622            case ContactEntryListFragment.ACTIVITY_REQUEST_CODE_PICKER:
1623                if (resultCode == RESULT_OK) {
1624                    mAllFragment.onPickerResult(data);
1625                }
1626
1627// TODO fix or remove multipicker code
1628//                else if (resultCode == RESULT_CANCELED && mMode == MODE_PICK_MULTIPLE_PHONES) {
1629//                    // Finish the activity if the sub activity was canceled as back key is used
1630//                    // to confirm user selection in MODE_PICK_MULTIPLE_PHONES.
1631//                    finish();
1632//                }
1633//                break;
1634        }
1635    }
1636
1637    @Override
1638    public boolean onKeyDown(int keyCode, KeyEvent event) {
1639        // TODO move to the fragment
1640        switch (keyCode) {
1641//            case KeyEvent.KEYCODE_CALL: {
1642//                if (callSelection()) {
1643//                    return true;
1644//                }
1645//                break;
1646//            }
1647
1648            case KeyEvent.KEYCODE_DEL: {
1649                if (deleteSelection()) {
1650                    return true;
1651                }
1652                break;
1653            }
1654            default: {
1655                // Bring up the search UI if the user starts typing
1656                final int unicodeChar = event.getUnicodeChar();
1657                if ((unicodeChar != 0)
1658                        // If COMBINING_ACCENT is set, it's not a unicode character.
1659                        && ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) == 0)
1660                        && !Character.isWhitespace(unicodeChar)) {
1661                    String query = new String(new int[]{ unicodeChar }, 0, 1);
1662                    if (!mActionBarAdapter.isSearchMode()) {
1663                        mActionBarAdapter.setQueryString(query);
1664                        mActionBarAdapter.setSearchMode(true);
1665                        return true;
1666                    }
1667                }
1668            }
1669        }
1670
1671        return super.onKeyDown(keyCode, event);
1672    }
1673
1674    @Override
1675    public void onBackPressed() {
1676        if (mActionBarAdapter.isSearchMode()) {
1677            mActionBarAdapter.setSearchMode(false);
1678        } else {
1679            super.onBackPressed();
1680        }
1681    }
1682
1683    private boolean deleteSelection() {
1684        // TODO move to the fragment
1685//        if (mActionCode == ContactsRequest.ACTION_DEFAULT) {
1686//            final int position = mListView.getSelectedItemPosition();
1687//            if (position != ListView.INVALID_POSITION) {
1688//                Uri contactUri = getContactUri(position);
1689//                if (contactUri != null) {
1690//                    doContactDelete(contactUri);
1691//                    return true;
1692//                }
1693//            }
1694//        }
1695        return false;
1696    }
1697
1698    @Override
1699    protected void onSaveInstanceState(Bundle outState) {
1700        super.onSaveInstanceState(outState);
1701        mActionBarAdapter.onSaveInstanceState(outState);
1702        if (mContactDetailLayoutController != null) {
1703            mContactDetailLayoutController.onSaveInstanceState(outState);
1704        }
1705
1706        // Clear the listener to make sure we don't get callbacks after onSaveInstanceState,
1707        // in order to avoid doing fragment transactions after it.
1708        // TODO Figure out a better way to deal with the issue.
1709        mActionBarAdapter.setListener(null);
1710        if (mTabPager != null) {
1711            mTabPager.setOnPageChangeListener(null);
1712        }
1713    }
1714
1715    @Override
1716    protected void onRestoreInstanceState(Bundle savedInstanceState) {
1717        super.onRestoreInstanceState(savedInstanceState);
1718        // In our own lifecycle, the focus is saved and restore but later taken away by the
1719        // ViewPager. As a hack, we force focus on the SearchView if we know that we are searching.
1720        // This fixes the keyboard going away on screen rotation
1721        if (mActionBarAdapter.isSearchMode()) {
1722            mActionBarAdapter.setFocusOnSearchView();
1723        }
1724    }
1725
1726    @Override
1727    public DialogManager getDialogManager() {
1728        return mDialogManager;
1729    }
1730
1731    // Visible for testing
1732    public ContactBrowseListFragment getListFragment() {
1733        return mAllFragment;
1734    }
1735
1736    // Visible for testing
1737    public ContactDetailFragment getDetailFragment() {
1738        return mContactDetailFragment;
1739    }
1740}
1741