• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.contacts.activities;
18 
19 import android.app.Fragment;
20 import android.app.FragmentManager;
21 import android.app.FragmentTransaction;
22 import android.content.ActivityNotFoundException;
23 import android.content.ContentValues;
24 import android.content.Intent;
25 import android.graphics.Rect;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.Parcelable;
30 import android.preference.PreferenceActivity;
31 import android.provider.ContactsContract;
32 import android.provider.ContactsContract.Contacts;
33 import android.provider.ContactsContract.ProviderStatus;
34 import android.provider.ContactsContract.QuickContact;
35 import android.provider.Settings;
36 import android.support.v13.app.FragmentPagerAdapter;
37 import android.support.v4.view.PagerAdapter;
38 import android.support.v4.view.ViewPager;
39 import android.util.Log;
40 import android.view.KeyCharacterMap;
41 import android.view.KeyEvent;
42 import android.view.Menu;
43 import android.view.MenuInflater;
44 import android.view.MenuItem;
45 import android.view.MenuItem.OnMenuItemClickListener;
46 import android.view.View;
47 import android.view.ViewGroup;
48 import android.widget.Toast;
49 
50 import com.android.contacts.ContactSaveService;
51 import com.android.contacts.ContactsActivity;
52 import com.android.contacts.ContactsUtils;
53 import com.android.contacts.R;
54 import com.android.contacts.activities.ActionBarAdapter.TabState;
55 import com.android.contacts.detail.ContactDetailFragment;
56 import com.android.contacts.detail.ContactDetailLayoutController;
57 import com.android.contacts.detail.ContactDetailUpdatesFragment;
58 import com.android.contacts.detail.ContactLoaderFragment;
59 import com.android.contacts.detail.ContactLoaderFragment.ContactLoaderFragmentListener;
60 import com.android.contacts.dialog.ClearFrequentsDialog;
61 import com.android.contacts.group.GroupBrowseListFragment;
62 import com.android.contacts.group.GroupBrowseListFragment.OnGroupBrowserActionListener;
63 import com.android.contacts.group.GroupDetailFragment;
64 import com.android.contacts.interactions.ContactDeletionInteraction;
65 import com.android.contacts.interactions.ImportExportDialogFragment;
66 import com.android.contacts.interactions.PhoneNumberInteraction;
67 import com.android.contacts.list.ContactBrowseListFragment;
68 import com.android.contacts.list.ContactEntryListFragment;
69 import com.android.contacts.list.ContactListFilter;
70 import com.android.contacts.list.ContactListFilterController;
71 import com.android.contacts.list.ContactTileAdapter.DisplayType;
72 import com.android.contacts.list.ContactTileFrequentFragment;
73 import com.android.contacts.list.ContactTileListFragment;
74 import com.android.contacts.list.ContactsIntentResolver;
75 import com.android.contacts.list.ContactsRequest;
76 import com.android.contacts.list.ContactsUnavailableFragment;
77 import com.android.contacts.list.DefaultContactBrowseListFragment;
78 import com.android.contacts.list.DirectoryListLoader;
79 import com.android.contacts.list.OnContactBrowserActionListener;
80 import com.android.contacts.list.OnContactsUnavailableActionListener;
81 import com.android.contacts.list.ProviderStatusWatcher;
82 import com.android.contacts.list.ProviderStatusWatcher.ProviderStatusListener;
83 import com.android.contacts.model.Contact;
84 import com.android.contacts.model.account.AccountWithDataSet;
85 import com.android.contacts.preference.ContactsPreferenceActivity;
86 import com.android.contacts.preference.DisplayOptionsPreferenceFragment;
87 import com.android.contacts.util.AccountFilterUtil;
88 import com.android.contacts.util.AccountPromptUtils;
89 import com.android.contacts.util.Constants;
90 import com.android.contacts.util.DialogManager;
91 import com.android.contacts.util.HelpUtils;
92 import com.android.contacts.util.PhoneCapabilityTester;
93 import com.android.contacts.util.UriUtils;
94 import com.android.contacts.widget.TransitionAnimationView;
95 
96 import java.util.ArrayList;
97 import java.util.concurrent.atomic.AtomicInteger;
98 
99 /**
100  * Displays a list to browse contacts. For xlarge screens, this also displays a detail-pane on
101  * the right.
102  */
103 public class PeopleActivity extends ContactsActivity
104         implements View.OnCreateContextMenuListener, ActionBarAdapter.Listener,
105         DialogManager.DialogShowingViewActivity,
106         ContactListFilterController.ContactListFilterListener, ProviderStatusListener {
107 
108     private static final String TAG = "PeopleActivity";
109 
110     /** Shows a toogle button for hiding/showing updates. Don't submit with true */
111     private static final boolean DEBUG_TRANSITIONS = false;
112 
113     private static final String ENABLE_DEBUG_OPTIONS_HIDDEN_CODE = "debug debug!";
114 
115     // These values needs to start at 2. See {@link ContactEntryListFragment}.
116     private static final int SUBACTIVITY_NEW_CONTACT = 2;
117     private static final int SUBACTIVITY_EDIT_CONTACT = 3;
118     private static final int SUBACTIVITY_NEW_GROUP = 4;
119     private static final int SUBACTIVITY_EDIT_GROUP = 5;
120     private static final int SUBACTIVITY_ACCOUNT_FILTER = 6;
121 
122     private final DialogManager mDialogManager = new DialogManager(this);
123 
124     private ContactsIntentResolver mIntentResolver;
125     private ContactsRequest mRequest;
126 
127     private ActionBarAdapter mActionBarAdapter;
128 
129     private ContactDetailFragment mContactDetailFragment;
130 
131     private ContactLoaderFragment mContactDetailLoaderFragment;
132     private final ContactDetailLoaderFragmentListener mContactDetailLoaderFragmentListener =
133             new ContactDetailLoaderFragmentListener();
134 
135     private GroupDetailFragment mGroupDetailFragment;
136     private final GroupDetailFragmentListener mGroupDetailFragmentListener =
137             new GroupDetailFragmentListener();
138 
139     private ContactTileListFragment.Listener mFavoritesFragmentListener =
140             new StrequentContactListFragmentListener();
141 
142     private ContactListFilterController mContactListFilterController;
143 
144     private ContactsUnavailableFragment mContactsUnavailableFragment;
145     private ProviderStatusWatcher mProviderStatusWatcher;
146     private ProviderStatusWatcher.Status mProviderStatus;
147 
148     private boolean mOptionsMenuContactsAvailable;
149 
150     /**
151      * Showing a list of Contacts. Also used for showing search results in search mode.
152      */
153     private DefaultContactBrowseListFragment mAllFragment;
154     private ContactTileListFragment mFavoritesFragment;
155     private ContactTileFrequentFragment mFrequentFragment;
156     private GroupBrowseListFragment mGroupsFragment;
157 
158     private View mFavoritesView;
159     private View mBrowserView;
160     private TransitionAnimationView mContactDetailsView;
161     private TransitionAnimationView mGroupDetailsView;
162 
163     /** ViewPager for swipe, used only on the phone (i.e. one-pane mode) */
164     private ViewPager mTabPager;
165     private TabPagerAdapter mTabPagerAdapter;
166     private final TabPagerListener mTabPagerListener = new TabPagerListener();
167 
168     private ContactDetailLayoutController mContactDetailLayoutController;
169 
170     private boolean mEnableDebugMenuOptions;
171 
172     private final Handler mHandler = new Handler();
173 
174     /**
175      * True if this activity instance is a re-created one.  i.e. set true after orientation change.
176      * This is set in {@link #onCreate} for later use in {@link #onStart}.
177      */
178     private boolean mIsRecreatedInstance;
179 
180     /**
181      * If {@link #configureFragments(boolean)} is already called.  Used to avoid calling it twice
182      * in {@link #onStart}.
183      * (This initialization only needs to be done once in onStart() when the Activity was just
184      * created from scratch -- i.e. onCreate() was just called)
185      */
186     private boolean mFragmentInitialized;
187 
188     /**
189      * Whether or not the current contact filter is valid or not. We need to do a check on
190      * start of the app to verify that the user is not in single contact mode. If so, we should
191      * dynamically change the filter, unless the incoming intent specifically requested a contact
192      * that should be displayed in that mode.
193      */
194     private boolean mCurrentFilterIsValid;
195 
196     /** Sequential ID assigned to each instance; used for logging */
197     private final int mInstanceId;
198     private static final AtomicInteger sNextInstanceId = new AtomicInteger();
199 
PeopleActivity()200     public PeopleActivity() {
201         mInstanceId = sNextInstanceId.getAndIncrement();
202         mIntentResolver = new ContactsIntentResolver(this);
203         mProviderStatusWatcher = ProviderStatusWatcher.getInstance(this);
204     }
205 
206     @Override
toString()207     public String toString() {
208         // Shown on logcat
209         return String.format("%s@%d", getClass().getSimpleName(), mInstanceId);
210     }
211 
areContactsAvailable()212     public boolean areContactsAvailable() {
213         return (mProviderStatus != null)
214                 && mProviderStatus.status == ProviderStatus.STATUS_NORMAL;
215     }
216 
areContactWritableAccountsAvailable()217     private boolean areContactWritableAccountsAvailable() {
218         return ContactsUtils.areContactWritableAccountsAvailable(this);
219     }
220 
areGroupWritableAccountsAvailable()221     private boolean areGroupWritableAccountsAvailable() {
222         return ContactsUtils.areGroupWritableAccountsAvailable(this);
223     }
224 
225     /**
226      * Initialize fragments that are (or may not be) in the layout.
227      *
228      * For the fragments that are in the layout, we initialize them in
229      * {@link #createViewsAndFragments(Bundle)} after inflating the layout.
230      *
231      * However, there are special fragments which may not be in the layout, so we have to do the
232      * initialization here.
233      * The target fragments are:
234      * - {@link ContactDetailFragment} and {@link ContactDetailUpdatesFragment}:  They may not be
235      *   in the layout depending on the configuration.  (i.e. portrait)
236      * - {@link ContactsUnavailableFragment}: We always create it at runtime.
237      */
238     @Override
onAttachFragment(Fragment fragment)239     public void onAttachFragment(Fragment fragment) {
240         if (fragment instanceof ContactDetailFragment) {
241             mContactDetailFragment = (ContactDetailFragment) fragment;
242         } else if (fragment instanceof ContactsUnavailableFragment) {
243             mContactsUnavailableFragment = (ContactsUnavailableFragment)fragment;
244             mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
245                     new ContactsUnavailableFragmentListener());
246         }
247     }
248 
249     @Override
onCreate(Bundle savedState)250     protected void onCreate(Bundle savedState) {
251         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
252             Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate start");
253         }
254         super.onCreate(savedState);
255 
256         if (!processIntent(false)) {
257             finish();
258             return;
259         }
260 
261         mContactListFilterController = ContactListFilterController.getInstance(this);
262         mContactListFilterController.checkFilterValidity(false);
263         mContactListFilterController.addListener(this);
264 
265         mProviderStatusWatcher.addListener(this);
266 
267         mIsRecreatedInstance = (savedState != null);
268         createViewsAndFragments(savedState);
269         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
270             Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate finish");
271         }
272     }
273 
274     @Override
onNewIntent(Intent intent)275     protected void onNewIntent(Intent intent) {
276         setIntent(intent);
277         if (!processIntent(true)) {
278             finish();
279             return;
280         }
281         mActionBarAdapter.initialize(null, mRequest);
282 
283         mContactListFilterController.checkFilterValidity(false);
284         mCurrentFilterIsValid = true;
285 
286         // Re-configure fragments.
287         configureFragments(true /* from request */);
288         invalidateOptionsMenuIfNeeded();
289     }
290 
291     /**
292      * Resolve the intent and initialize {@link #mRequest}, and launch another activity if redirect
293      * is needed.
294      *
295      * @param forNewIntent set true if it's called from {@link #onNewIntent(Intent)}.
296      * @return {@code true} if {@link PeopleActivity} should continue running.  {@code false}
297      *         if it shouldn't, in which case the caller should finish() itself and shouldn't do
298      *         farther initialization.
299      */
processIntent(boolean forNewIntent)300     private boolean processIntent(boolean forNewIntent) {
301         // Extract relevant information from the intent
302         mRequest = mIntentResolver.resolveIntent(getIntent());
303         if (Log.isLoggable(TAG, Log.DEBUG)) {
304             Log.d(TAG, this + " processIntent: forNewIntent=" + forNewIntent
305                     + " intent=" + getIntent() + " request=" + mRequest);
306         }
307         if (!mRequest.isValid()) {
308             setResult(RESULT_CANCELED);
309             return false;
310         }
311 
312         Intent redirect = mRequest.getRedirectIntent();
313         if (redirect != null) {
314             // Need to start a different activity
315             startActivity(redirect);
316             return false;
317         }
318 
319         if (mRequest.getActionCode() == ContactsRequest.ACTION_VIEW_CONTACT
320                 && !PhoneCapabilityTester.isUsingTwoPanes(this)) {
321             redirect = new Intent(this, ContactDetailActivity.class);
322             redirect.setAction(Intent.ACTION_VIEW);
323             redirect.setData(mRequest.getContactUri());
324             startActivity(redirect);
325             return false;
326         }
327         return true;
328     }
329 
createViewsAndFragments(Bundle savedState)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
onStart()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
onPause()484     protected void onPause() {
485         mOptionsMenuContactsAvailable = false;
486         mProviderStatusWatcher.stop();
487         super.onPause();
488     }
489 
490     @Override
onResume()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
onStop()509     protected void onStop() {
510         super.onStop();
511         mCurrentFilterIsValid = false;
512     }
513 
514     @Override
onDestroy()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 
configureFragments(boolean fromRequest)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
onContactListFilterChanged()594     public void onContactListFilterChanged() {
595         if (mAllFragment == null || !mAllFragment.isAdded()) {
596             return;
597         }
598 
599         mAllFragment.setFilter(mContactListFilterController.getFilter());
600 
601         invalidateOptionsMenuIfNeeded();
602     }
603 
setupContactDetailFragment(final Uri contactLookupUri)604     private void setupContactDetailFragment(final Uri contactLookupUri) {
605         mContactDetailLoaderFragment.loadUri(contactLookupUri);
606         invalidateOptionsMenuIfNeeded();
607     }
608 
setupGroupDetailFragment(Uri groupUri)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
onAction(int action)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
onSelectedTabChanged()648     public void onSelectedTabChanged() {
649         updateFragmentsVisibility();
650     }
651 
updateDebugOptionsVisibility(boolean visible)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      */
updateFragmentsVisibility()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 
showEmptyStateForTab(int tab)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.
TabPagerListener()789         TabPagerListener() {}
790 
791         @Override
onPageScrollStateChanged(int state)792         public void onPageScrollStateChanged(int state) {
793         }
794 
795         @Override
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)796         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
797         }
798 
799         @Override
onPageSelected(int position)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 
TabPagerAdapter()831         public TabPagerAdapter() {
832             mFragmentManager = getFragmentManager();
833         }
834 
isSearchMode()835         public boolean isSearchMode() {
836             return mTabPagerAdapterSearchMode;
837         }
838 
setSearchMode(boolean searchMode)839         public void setSearchMode(boolean searchMode) {
840             if (searchMode == mTabPagerAdapterSearchMode) {
841                 return;
842             }
843             mTabPagerAdapterSearchMode = searchMode;
844             notifyDataSetChanged();
845         }
846 
847         @Override
getCount()848         public int getCount() {
849             return mTabPagerAdapterSearchMode ? 1 : TabState.COUNT;
850         }
851 
852         /** Gets called when the number of items changes. */
853         @Override
getItemPosition(Object object)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
startUpdate(ViewGroup container)874         public void startUpdate(ViewGroup container) {
875         }
876 
getFragment(int position)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
instantiateItem(ViewGroup container, int position)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
destroyItem(ViewGroup container, int position, Object object)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
finishUpdate(ViewGroup container)920         public void finishUpdate(ViewGroup container) {
921             if (mCurTransaction != null) {
922                 mCurTransaction.commitAllowingStateLoss();
923                 mCurTransaction = null;
924                 mFragmentManager.executePendingTransactions();
925             }
926         }
927 
928         @Override
isViewFromObject(View view, Object object)929         public boolean isViewFromObject(View view, Object object) {
930             return ((Fragment) object).getView() == view;
931         }
932 
933         @Override
setPrimaryItem(ViewGroup container, int position, Object object)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
saveState()948         public Parcelable saveState() {
949             return null;
950         }
951 
952         @Override
restoreState(Parcelable state, ClassLoader loader)953         public void restoreState(Parcelable state, ClassLoader loader) {
954         }
955     }
956 
setQueryTextToFragment(String query)957     private void setQueryTextToFragment(String query) {
958         mAllFragment.setQueryString(query, true);
959         mAllFragment.setVisibleScrollbarEnabled(!mAllFragment.isSearchMode());
960     }
961 
configureContactListFragmentForRequest()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 
configureContactListFragment()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 
configureGroupListFragment()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
onProviderStatusChange()1007     public void onProviderStatusChange() {
1008         updateViewConfiguration(false);
1009     }
1010 
updateViewConfiguration(boolean forceUpdate)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 {
ContactBrowserActionListener()1076         ContactBrowserActionListener() {}
1077 
1078         @Override
onSelectionChange()1079         public void onSelectionChange() {
1080             if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) {
1081                 setupContactDetailFragment(mAllFragment.getSelectedContactUri());
1082             }
1083         }
1084 
1085         @Override
onViewContactAction(Uri contactLookupUri)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
onCreateNewContactAction()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
onEditContactAction(Uri contactLookupUri)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
onAddToFavoritesAction(Uri contactUri)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
onRemoveFromFavoritesAction(Uri contactUri)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
onCallContactAction(Uri contactUri)1132         public void onCallContactAction(Uri contactUri) {
1133             PhoneNumberInteraction.startInteractionForPhoneCall(PeopleActivity.this, contactUri);
1134         }
1135 
1136         @Override
onSmsContactAction(Uri contactUri)1137         public void onSmsContactAction(Uri contactUri) {
1138             PhoneNumberInteraction.startInteractionForTextMessage(PeopleActivity.this, contactUri);
1139         }
1140 
1141         @Override
onDeleteContactAction(Uri contactUri)1142         public void onDeleteContactAction(Uri contactUri) {
1143             ContactDeletionInteraction.start(PeopleActivity.this, contactUri, false);
1144         }
1145 
1146         @Override
onFinishAction()1147         public void onFinishAction() {
1148             onBackPressed();
1149         }
1150 
1151         @Override
onInvalidSelection()1152         public void onInvalidSelection() {
1153             ContactListFilter filter;
1154             ContactListFilter currentFilter = mAllFragment.getFilter();
1155             if (currentFilter != null
1156                     && currentFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
1157                 filter = ContactListFilter.createFilterWithType(
1158                         ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
1159                 mAllFragment.setFilter(filter);
1160             } else {
1161                 filter = ContactListFilter.createFilterWithType(
1162                         ContactListFilter.FILTER_TYPE_SINGLE_CONTACT);
1163                 mAllFragment.setFilter(filter, false);
1164             }
1165             mContactListFilterController.setContactListFilter(filter, true);
1166         }
1167     }
1168 
1169     private class ContactDetailLoaderFragmentListener implements ContactLoaderFragmentListener {
ContactDetailLoaderFragmentListener()1170         ContactDetailLoaderFragmentListener() {}
1171 
1172         @Override
onContactNotFound()1173         public void onContactNotFound() {
1174             // Nothing needs to be done here
1175         }
1176 
1177         @Override
onDetailsLoaded(final Contact result)1178         public void onDetailsLoaded(final Contact result) {
1179             if (result == null) {
1180                 // Nothing is loaded. Show empty state.
1181                 mContactDetailLayoutController.showEmptyState();
1182                 return;
1183             }
1184             // Since {@link FragmentTransaction}s cannot be done in the onLoadFinished() of the
1185             // {@link LoaderCallbacks}, then post this {@link Runnable} to the {@link Handler}
1186             // on the main thread to execute later.
1187             mHandler.post(new Runnable() {
1188                 @Override
1189                 public void run() {
1190                     // If the activity is destroyed (or will be destroyed soon), don't update the UI
1191                     if (isFinishing()) {
1192                         return;
1193                     }
1194                     mContactDetailLayoutController.setContactData(result);
1195                 }
1196             });
1197         }
1198 
1199         @Override
onEditRequested(Uri contactLookupUri)1200         public void onEditRequested(Uri contactLookupUri) {
1201             Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri);
1202             intent.putExtra(
1203                     ContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, true);
1204             startActivityForResult(intent, SUBACTIVITY_EDIT_CONTACT);
1205         }
1206 
1207         @Override
onDeleteRequested(Uri contactUri)1208         public void onDeleteRequested(Uri contactUri) {
1209             ContactDeletionInteraction.start(PeopleActivity.this, contactUri, false);
1210         }
1211     }
1212 
1213     public class ContactDetailFragmentListener implements ContactDetailFragment.Listener {
1214         @Override
onItemClicked(Intent intent)1215         public void onItemClicked(Intent intent) {
1216             if (intent == null) {
1217                 return;
1218             }
1219             try {
1220                 startActivity(intent);
1221             } catch (ActivityNotFoundException e) {
1222                 Log.e(TAG, "No activity found for intent: " + intent);
1223             }
1224         }
1225 
1226         @Override
onCreateRawContactRequested(ArrayList<ContentValues> values, AccountWithDataSet account)1227         public void onCreateRawContactRequested(ArrayList<ContentValues> values,
1228                 AccountWithDataSet account) {
1229             Toast.makeText(PeopleActivity.this, R.string.toast_making_personal_copy,
1230                     Toast.LENGTH_LONG).show();
1231             Intent serviceIntent = ContactSaveService.createNewRawContactIntent(
1232                     PeopleActivity.this, values, account,
1233                     PeopleActivity.class, Intent.ACTION_VIEW);
1234             startService(serviceIntent);
1235         }
1236     }
1237 
1238     private class ContactsUnavailableFragmentListener
1239             implements OnContactsUnavailableActionListener {
ContactsUnavailableFragmentListener()1240         ContactsUnavailableFragmentListener() {}
1241 
1242         @Override
onCreateNewContactAction()1243         public void onCreateNewContactAction() {
1244             startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
1245         }
1246 
1247         @Override
onAddAccountAction()1248         public void onAddAccountAction() {
1249             Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
1250             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
1251             intent.putExtra(Settings.EXTRA_AUTHORITIES,
1252                     new String[] { ContactsContract.AUTHORITY });
1253             startActivity(intent);
1254         }
1255 
1256         @Override
onImportContactsFromFileAction()1257         public void onImportContactsFromFileAction() {
1258             ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable());
1259         }
1260 
1261         @Override
onFreeInternalStorageAction()1262         public void onFreeInternalStorageAction() {
1263             startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
1264         }
1265     }
1266 
1267     private final class StrequentContactListFragmentListener
1268             implements ContactTileListFragment.Listener {
StrequentContactListFragmentListener()1269         StrequentContactListFragmentListener() {}
1270 
1271         @Override
onContactSelected(Uri contactUri, Rect targetRect)1272         public void onContactSelected(Uri contactUri, Rect targetRect) {
1273             if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) {
1274                 QuickContact.showQuickContact(PeopleActivity.this, targetRect, contactUri, 0, null);
1275             } else {
1276                 startActivity(new Intent(Intent.ACTION_VIEW, contactUri));
1277             }
1278         }
1279 
1280         @Override
onCallNumberDirectly(String phoneNumber)1281         public void onCallNumberDirectly(String phoneNumber) {
1282             // No need to call phone number directly from People app.
1283             Log.w(TAG, "unexpected invocation of onCallNumberDirectly()");
1284         }
1285     }
1286 
1287     private final class GroupBrowserActionListener implements OnGroupBrowserActionListener {
1288 
GroupBrowserActionListener()1289         GroupBrowserActionListener() {}
1290 
1291         @Override
onViewGroupAction(Uri groupUri)1292         public void onViewGroupAction(Uri groupUri) {
1293             if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) {
1294                 setupGroupDetailFragment(groupUri);
1295             } else {
1296                 Intent intent = new Intent(PeopleActivity.this, GroupDetailActivity.class);
1297                 intent.setData(groupUri);
1298                 startActivity(intent);
1299             }
1300         }
1301     }
1302 
1303     private class GroupDetailFragmentListener implements GroupDetailFragment.Listener {
1304 
GroupDetailFragmentListener()1305         GroupDetailFragmentListener() {}
1306 
1307         @Override
onGroupSizeUpdated(String size)1308         public void onGroupSizeUpdated(String size) {
1309             // Nothing needs to be done here because the size will be displayed in the detail
1310             // fragment
1311         }
1312 
1313         @Override
onGroupTitleUpdated(String title)1314         public void onGroupTitleUpdated(String title) {
1315             // Nothing needs to be done here because the title will be displayed in the detail
1316             // fragment
1317         }
1318 
1319         @Override
onAccountTypeUpdated(String accountTypeString, String dataSet)1320         public void onAccountTypeUpdated(String accountTypeString, String dataSet) {
1321             // Nothing needs to be done here because the group source will be displayed in the
1322             // detail fragment
1323         }
1324 
1325         @Override
onEditRequested(Uri groupUri)1326         public void onEditRequested(Uri groupUri) {
1327             final Intent intent = new Intent(PeopleActivity.this, GroupEditorActivity.class);
1328             intent.setData(groupUri);
1329             intent.setAction(Intent.ACTION_EDIT);
1330             startActivityForResult(intent, SUBACTIVITY_EDIT_GROUP);
1331         }
1332 
1333         @Override
onContactSelected(Uri contactUri)1334         public void onContactSelected(Uri contactUri) {
1335             // Nothing needs to be done here because either quickcontact will be displayed
1336             // or activity will take care of selection
1337         }
1338     }
1339 
startActivityAndForwardResult(final Intent intent)1340     public void startActivityAndForwardResult(final Intent intent) {
1341         intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
1342 
1343         // Forward extras to the new activity
1344         Bundle extras = getIntent().getExtras();
1345         if (extras != null) {
1346             intent.putExtras(extras);
1347         }
1348         startActivity(intent);
1349         finish();
1350     }
1351 
1352     @Override
onCreateOptionsMenu(Menu menu)1353     public boolean onCreateOptionsMenu(Menu menu) {
1354         if (!areContactsAvailable()) {
1355             // If contacts aren't available, hide all menu items.
1356             return false;
1357         }
1358         super.onCreateOptionsMenu(menu);
1359 
1360         MenuInflater inflater = getMenuInflater();
1361         inflater.inflate(R.menu.people_options, menu);
1362 
1363         if (DEBUG_TRANSITIONS && mContactDetailLoaderFragment != null) {
1364             final MenuItem toggleSocial =
1365                     menu.add(mContactDetailLoaderFragment.getLoadStreamItems() ? "less" : "more");
1366             toggleSocial.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1367             toggleSocial.setOnMenuItemClickListener(new OnMenuItemClickListener() {
1368                 @Override
1369                 public boolean onMenuItemClick(MenuItem item) {
1370                     mContactDetailLoaderFragment.toggleLoadStreamItems();
1371                     invalidateOptionsMenu();
1372                     return false;
1373                 }
1374             });
1375         }
1376 
1377         return true;
1378     }
1379 
invalidateOptionsMenuIfNeeded()1380     private void invalidateOptionsMenuIfNeeded() {
1381         if (isOptionsMenuChanged()) {
1382             invalidateOptionsMenu();
1383         }
1384     }
1385 
isOptionsMenuChanged()1386     public boolean isOptionsMenuChanged() {
1387         if (mOptionsMenuContactsAvailable != areContactsAvailable()) {
1388             return true;
1389         }
1390 
1391         if (mAllFragment != null && mAllFragment.isOptionsMenuChanged()) {
1392             return true;
1393         }
1394 
1395         if (mContactDetailLoaderFragment != null &&
1396                 mContactDetailLoaderFragment.isOptionsMenuChanged()) {
1397             return true;
1398         }
1399 
1400         if (mGroupDetailFragment != null && mGroupDetailFragment.isOptionsMenuChanged()) {
1401             return true;
1402         }
1403 
1404         return false;
1405     }
1406 
1407     @Override
onPrepareOptionsMenu(Menu menu)1408     public boolean onPrepareOptionsMenu(Menu menu) {
1409         mOptionsMenuContactsAvailable = areContactsAvailable();
1410         if (!mOptionsMenuContactsAvailable) {
1411             return false;
1412         }
1413 
1414         // Get references to individual menu items in the menu
1415         final MenuItem addContactMenu = menu.findItem(R.id.menu_add_contact);
1416         final MenuItem contactsFilterMenu = menu.findItem(R.id.menu_contacts_filter);
1417 
1418         MenuItem addGroupMenu = menu.findItem(R.id.menu_add_group);
1419 
1420         final MenuItem clearFrequentsMenu = menu.findItem(R.id.menu_clear_frequents);
1421         final MenuItem helpMenu = menu.findItem(R.id.menu_help);
1422 
1423         final boolean isSearchMode = mActionBarAdapter.isSearchMode();
1424         if (isSearchMode) {
1425             addContactMenu.setVisible(false);
1426             addGroupMenu.setVisible(false);
1427             contactsFilterMenu.setVisible(false);
1428             clearFrequentsMenu.setVisible(false);
1429             helpMenu.setVisible(false);
1430         } else {
1431             switch (mActionBarAdapter.getCurrentTab()) {
1432                 case TabState.FAVORITES:
1433                     addContactMenu.setVisible(false);
1434                     addGroupMenu.setVisible(false);
1435                     contactsFilterMenu.setVisible(false);
1436                     clearFrequentsMenu.setVisible(hasFrequents());
1437                     break;
1438                 case TabState.ALL:
1439                     addContactMenu.setVisible(true);
1440                     addGroupMenu.setVisible(false);
1441                     contactsFilterMenu.setVisible(true);
1442                     clearFrequentsMenu.setVisible(false);
1443                     break;
1444                 case TabState.GROUPS:
1445                     // Do not display the "new group" button if no accounts are available
1446                     if (areGroupWritableAccountsAvailable()) {
1447                         addGroupMenu.setVisible(true);
1448                     } else {
1449                         addGroupMenu.setVisible(false);
1450                     }
1451                     addContactMenu.setVisible(false);
1452                     contactsFilterMenu.setVisible(false);
1453                     clearFrequentsMenu.setVisible(false);
1454                     break;
1455             }
1456             HelpUtils.prepareHelpMenuItem(this, helpMenu, R.string.help_url_people_main);
1457         }
1458         final boolean showMiscOptions = !isSearchMode;
1459         makeMenuItemVisible(menu, R.id.menu_search, showMiscOptions);
1460         makeMenuItemVisible(menu, R.id.menu_import_export, showMiscOptions);
1461         makeMenuItemVisible(menu, R.id.menu_accounts, showMiscOptions);
1462         makeMenuItemVisible(menu, R.id.menu_settings,
1463                 showMiscOptions && !ContactsPreferenceActivity.isEmpty(this));
1464 
1465         // Debug options need to be visible even in search mode.
1466         makeMenuItemVisible(menu, R.id.export_database, mEnableDebugMenuOptions);
1467 
1468         return true;
1469     }
1470 
1471     /**
1472      * Returns whether there are any frequently contacted people being displayed
1473      * @return
1474      */
hasFrequents()1475     private boolean hasFrequents() {
1476         if (PhoneCapabilityTester.isUsingTwoPanesInFavorites(this)) {
1477             return mFrequentFragment.hasFrequents();
1478         } else {
1479             return mFavoritesFragment.hasFrequents();
1480         }
1481     }
1482 
makeMenuItemVisible(Menu menu, int itemId, boolean visible)1483     private void makeMenuItemVisible(Menu menu, int itemId, boolean visible) {
1484         MenuItem item =menu.findItem(itemId);
1485         if (item != null) {
1486             item.setVisible(visible);
1487         }
1488     }
1489 
1490     @Override
onOptionsItemSelected(MenuItem item)1491     public boolean onOptionsItemSelected(MenuItem item) {
1492         switch (item.getItemId()) {
1493             case android.R.id.home: {
1494                 // The home icon on the action bar is pressed
1495                 if (mActionBarAdapter.isUpShowing()) {
1496                     // "UP" icon press -- should be treated as "back".
1497                     onBackPressed();
1498                 }
1499                 return true;
1500             }
1501             case R.id.menu_settings: {
1502                 final Intent intent = new Intent(this, ContactsPreferenceActivity.class);
1503                 // as there is only one section right now, make sure it is selected
1504                 // on small screens, this also hides the section selector
1505                 // Due to b/5045558, this code unfortunately only works properly on phones
1506                 boolean settingsAreMultiPane = getResources().getBoolean(
1507                         com.android.internal.R.bool.preferences_prefer_dual_pane);
1508                 if (!settingsAreMultiPane) {
1509                     intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT,
1510                             DisplayOptionsPreferenceFragment.class.getName());
1511                     intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE,
1512                             R.string.preference_displayOptions);
1513                 }
1514                 startActivity(intent);
1515                 return true;
1516             }
1517             case R.id.menu_contacts_filter: {
1518                 AccountFilterUtil.startAccountFilterActivityForResult(
1519                         this, SUBACTIVITY_ACCOUNT_FILTER,
1520                         mContactListFilterController.getFilter());
1521                 return true;
1522             }
1523             case R.id.menu_search: {
1524                 onSearchRequested();
1525                 return true;
1526             }
1527             case R.id.menu_add_contact: {
1528                 final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
1529                 // On 2-pane UI, we can let the editor activity finish itself and return
1530                 // to this activity to display the new contact.
1531                 if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
1532                     intent.putExtra(
1533                             ContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED,
1534                             true);
1535                     startActivityForResult(intent, SUBACTIVITY_NEW_CONTACT);
1536                 } else {
1537                     // Otherwise, on 1-pane UI, we need the editor to launch the view contact
1538                     // intent itself.
1539                     startActivity(intent);
1540                 }
1541                 return true;
1542             }
1543             case R.id.menu_add_group: {
1544                 createNewGroup();
1545                 return true;
1546             }
1547             case R.id.menu_import_export: {
1548                 ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable());
1549                 return true;
1550             }
1551             case R.id.menu_clear_frequents: {
1552                 ClearFrequentsDialog.show(getFragmentManager());
1553                 return true;
1554             }
1555             case R.id.menu_accounts: {
1556                 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
1557                 intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] {
1558                     ContactsContract.AUTHORITY
1559                 });
1560                 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
1561                 startActivity(intent);
1562                 return true;
1563             }
1564             case R.id.export_database: {
1565                 final Intent intent = new Intent("com.android.providers.contacts.DUMP_DATABASE");
1566                 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
1567                 startActivity(intent);
1568                 return true;
1569             }
1570         }
1571         return false;
1572     }
1573 
createNewGroup()1574     private void createNewGroup() {
1575         final Intent intent = new Intent(this, GroupEditorActivity.class);
1576         intent.setAction(Intent.ACTION_INSERT);
1577         startActivityForResult(intent, SUBACTIVITY_NEW_GROUP);
1578     }
1579 
1580     @Override
onSearchRequested()1581     public boolean onSearchRequested() { // Search key pressed.
1582         mActionBarAdapter.setSearchMode(true);
1583         return true;
1584     }
1585 
1586     @Override
onActivityResult(int requestCode, int resultCode, Intent data)1587     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1588         switch (requestCode) {
1589             case SUBACTIVITY_ACCOUNT_FILTER: {
1590                 AccountFilterUtil.handleAccountFilterResult(
1591                         mContactListFilterController, resultCode, data);
1592                 break;
1593             }
1594 
1595             case SUBACTIVITY_NEW_CONTACT:
1596             case SUBACTIVITY_EDIT_CONTACT: {
1597                 if (resultCode == RESULT_OK && PhoneCapabilityTester.isUsingTwoPanes(this)) {
1598                     mRequest.setActionCode(ContactsRequest.ACTION_VIEW_CONTACT);
1599                     mAllFragment.setSelectionRequired(true);
1600                     mAllFragment.setSelectedContactUri(data.getData());
1601                     // Suppress IME if in search mode
1602                     if (mActionBarAdapter != null) {
1603                         mActionBarAdapter.clearFocusOnSearchView();
1604                     }
1605                     // No need to change the contact filter
1606                     mCurrentFilterIsValid = true;
1607                 }
1608                 break;
1609             }
1610 
1611             case SUBACTIVITY_NEW_GROUP:
1612             case SUBACTIVITY_EDIT_GROUP: {
1613                 if (resultCode == RESULT_OK && PhoneCapabilityTester.isUsingTwoPanes(this)) {
1614                     mRequest.setActionCode(ContactsRequest.ACTION_GROUP);
1615                     mGroupsFragment.setSelectedUri(data.getData());
1616                 }
1617                 break;
1618             }
1619 
1620             // TODO: Using the new startActivityWithResultFromFragment API this should not be needed
1621             // anymore
1622             case ContactEntryListFragment.ACTIVITY_REQUEST_CODE_PICKER:
1623                 if (resultCode == RESULT_OK) {
1624                     mAllFragment.onPickerResult(data);
1625                 }
1626 
1627 // TODO fix or remove multipicker code
1628 //                else if (resultCode == RESULT_CANCELED && mMode == MODE_PICK_MULTIPLE_PHONES) {
1629 //                    // Finish the activity if the sub activity was canceled as back key is used
1630 //                    // to confirm user selection in MODE_PICK_MULTIPLE_PHONES.
1631 //                    finish();
1632 //                }
1633 //                break;
1634         }
1635     }
1636 
1637     @Override
onKeyDown(int keyCode, KeyEvent event)1638     public boolean onKeyDown(int keyCode, KeyEvent event) {
1639         // TODO move to the fragment
1640         switch (keyCode) {
1641 //            case KeyEvent.KEYCODE_CALL: {
1642 //                if (callSelection()) {
1643 //                    return true;
1644 //                }
1645 //                break;
1646 //            }
1647 
1648             case KeyEvent.KEYCODE_DEL: {
1649                 if (deleteSelection()) {
1650                     return true;
1651                 }
1652                 break;
1653             }
1654             default: {
1655                 // Bring up the search UI if the user starts typing
1656                 final int unicodeChar = event.getUnicodeChar();
1657                 if ((unicodeChar != 0)
1658                         // If COMBINING_ACCENT is set, it's not a unicode character.
1659                         && ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) == 0)
1660                         && !Character.isWhitespace(unicodeChar)) {
1661                     String query = new String(new int[]{ unicodeChar }, 0, 1);
1662                     if (!mActionBarAdapter.isSearchMode()) {
1663                         mActionBarAdapter.setQueryString(query);
1664                         mActionBarAdapter.setSearchMode(true);
1665                         return true;
1666                     }
1667                 }
1668             }
1669         }
1670 
1671         return super.onKeyDown(keyCode, event);
1672     }
1673 
1674     @Override
onBackPressed()1675     public void onBackPressed() {
1676         if (mActionBarAdapter.isSearchMode()) {
1677             mActionBarAdapter.setSearchMode(false);
1678         } else {
1679             super.onBackPressed();
1680         }
1681     }
1682 
deleteSelection()1683     private boolean deleteSelection() {
1684         // TODO move to the fragment
1685 //        if (mActionCode == ContactsRequest.ACTION_DEFAULT) {
1686 //            final int position = mListView.getSelectedItemPosition();
1687 //            if (position != ListView.INVALID_POSITION) {
1688 //                Uri contactUri = getContactUri(position);
1689 //                if (contactUri != null) {
1690 //                    doContactDelete(contactUri);
1691 //                    return true;
1692 //                }
1693 //            }
1694 //        }
1695         return false;
1696     }
1697 
1698     @Override
onSaveInstanceState(Bundle outState)1699     protected void onSaveInstanceState(Bundle outState) {
1700         super.onSaveInstanceState(outState);
1701         mActionBarAdapter.onSaveInstanceState(outState);
1702         if (mContactDetailLayoutController != null) {
1703             mContactDetailLayoutController.onSaveInstanceState(outState);
1704         }
1705 
1706         // Clear the listener to make sure we don't get callbacks after onSaveInstanceState,
1707         // in order to avoid doing fragment transactions after it.
1708         // TODO Figure out a better way to deal with the issue.
1709         mActionBarAdapter.setListener(null);
1710         if (mTabPager != null) {
1711             mTabPager.setOnPageChangeListener(null);
1712         }
1713     }
1714 
1715     @Override
onRestoreInstanceState(Bundle savedInstanceState)1716     protected void onRestoreInstanceState(Bundle savedInstanceState) {
1717         super.onRestoreInstanceState(savedInstanceState);
1718         // In our own lifecycle, the focus is saved and restore but later taken away by the
1719         // ViewPager. As a hack, we force focus on the SearchView if we know that we are searching.
1720         // This fixes the keyboard going away on screen rotation
1721         if (mActionBarAdapter.isSearchMode()) {
1722             mActionBarAdapter.setFocusOnSearchView();
1723         }
1724     }
1725 
1726     @Override
getDialogManager()1727     public DialogManager getDialogManager() {
1728         return mDialogManager;
1729     }
1730 
1731     // Visible for testing
getListFragment()1732     public ContactBrowseListFragment getListFragment() {
1733         return mAllFragment;
1734     }
1735 
1736     // Visible for testing
getDetailFragment()1737     public ContactDetailFragment getDetailFragment() {
1738         return mContactDetailFragment;
1739     }
1740 }
1741