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