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