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