1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.contacts.activities; 18 19import android.app.ActionBar; 20import android.app.ActionBar.LayoutParams; 21import android.app.Activity; 22import android.app.Fragment; 23import android.content.ActivityNotFoundException; 24import android.content.Context; 25import android.content.Intent; 26import android.net.Uri; 27import android.os.Bundle; 28import android.provider.ContactsContract.Contacts; 29import android.provider.ContactsContract.Intents.Insert; 30import android.provider.ContactsContract.Intents.UI; 31import android.text.TextUtils; 32import android.util.Log; 33import android.view.LayoutInflater; 34import android.view.Menu; 35import android.view.MenuInflater; 36import android.view.MenuItem; 37import android.view.View; 38import android.view.View.OnClickListener; 39import android.view.View.OnFocusChangeListener; 40import android.view.inputmethod.InputMethodManager; 41import android.widget.SearchView; 42import android.widget.SearchView.OnCloseListener; 43import android.widget.SearchView.OnQueryTextListener; 44import android.widget.Toast; 45 46import com.android.contacts.ContactsActivity; 47import com.android.contacts.R; 48import com.android.contacts.common.list.ContactEntryListFragment; 49import com.android.contacts.list.ContactPickerFragment; 50import com.android.contacts.list.ContactsIntentResolver; 51import com.android.contacts.list.ContactsRequest; 52import com.android.contacts.common.list.DirectoryListLoader; 53import com.android.contacts.list.EmailAddressPickerFragment; 54import com.android.contacts.list.JoinContactListFragment; 55import com.android.contacts.list.LegacyPhoneNumberPickerFragment; 56import com.android.contacts.list.OnContactPickerActionListener; 57import com.android.contacts.list.OnEmailAddressPickerActionListener; 58import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; 59import com.android.contacts.list.OnPostalAddressPickerActionListener; 60import com.android.contacts.common.list.PhoneNumberPickerFragment; 61import com.android.contacts.list.PostalAddressPickerFragment; 62import com.google.common.collect.Sets; 63 64import java.util.Set; 65 66/** 67 * Displays a list of contacts (or phone numbers or postal addresses) for the 68 * purposes of selecting one. 69 */ 70public class ContactSelectionActivity extends ContactsActivity 71 implements View.OnCreateContextMenuListener, OnQueryTextListener, OnClickListener, 72 OnCloseListener, OnFocusChangeListener { 73 private static final String TAG = "ContactSelectionActivity"; 74 75 private static final int SUBACTIVITY_ADD_TO_EXISTING_CONTACT = 0; 76 77 private static final String KEY_ACTION_CODE = "actionCode"; 78 private static final String KEY_SEARCH_MODE = "searchMode"; 79 private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20; 80 81 private ContactsIntentResolver mIntentResolver; 82 protected ContactEntryListFragment<?> mListFragment; 83 84 private int mActionCode = -1; 85 private boolean mIsSearchMode; 86 87 private ContactsRequest mRequest; 88 private SearchView mSearchView; 89 private View mSearchViewContainer; 90 91 public ContactSelectionActivity() { 92 mIntentResolver = new ContactsIntentResolver(this); 93 } 94 95 @Override 96 public void onAttachFragment(Fragment fragment) { 97 if (fragment instanceof ContactEntryListFragment<?>) { 98 mListFragment = (ContactEntryListFragment<?>) fragment; 99 setupActionListener(); 100 } 101 } 102 103 @Override 104 protected void onCreate(Bundle savedState) { 105 super.onCreate(savedState); 106 107 if (savedState != null) { 108 mActionCode = savedState.getInt(KEY_ACTION_CODE); 109 mIsSearchMode = savedState.getBoolean(KEY_SEARCH_MODE); 110 } 111 112 // Extract relevant information from the intent 113 mRequest = mIntentResolver.resolveIntent(getIntent()); 114 if (!mRequest.isValid()) { 115 setResult(RESULT_CANCELED); 116 finish(); 117 return; 118 } 119 120 Intent redirect = mRequest.getRedirectIntent(); 121 if (redirect != null) { 122 // Need to start a different activity 123 startActivity(redirect); 124 finish(); 125 return; 126 } 127 128 configureActivityTitle(); 129 130 setContentView(R.layout.contact_picker); 131 132 if (mActionCode != mRequest.getActionCode()) { 133 mActionCode = mRequest.getActionCode(); 134 configureListFragment(); 135 } 136 137 prepareSearchViewAndActionBar(); 138 } 139 140 private void prepareSearchViewAndActionBar() { 141 final ActionBar actionBar = getActionBar(); 142 mSearchViewContainer = LayoutInflater.from(actionBar.getThemedContext()) 143 .inflate(R.layout.custom_action_bar, null); 144 mSearchView = (SearchView) mSearchViewContainer.findViewById(R.id.search_view); 145 146 // Postal address pickers (and legacy pickers) don't support search, so just show 147 // "HomeAsUp" button and title. 148 if (mRequest.getActionCode() == ContactsRequest.ACTION_PICK_POSTAL || 149 mRequest.isLegacyCompatibilityMode()) { 150 mSearchView.setVisibility(View.GONE); 151 if (actionBar != null) { 152 actionBar.setDisplayShowHomeEnabled(true); 153 actionBar.setDisplayHomeAsUpEnabled(true); 154 actionBar.setDisplayShowTitleEnabled(true); 155 } 156 return; 157 } 158 159 actionBar.setDisplayShowHomeEnabled(true); 160 actionBar.setDisplayHomeAsUpEnabled(true); 161 162 // In order to make the SearchView look like "shown via search menu", we need to 163 // manually setup its state. See also DialtactsActivity.java and ActionBarAdapter.java. 164 mSearchView.setIconifiedByDefault(true); 165 mSearchView.setQueryHint(getString(R.string.hint_findContacts)); 166 mSearchView.setIconified(false); 167 mSearchView.setFocusable(true); 168 169 mSearchView.setOnQueryTextListener(this); 170 mSearchView.setOnCloseListener(this); 171 mSearchView.setOnQueryTextFocusChangeListener(this); 172 173 actionBar.setCustomView(mSearchViewContainer, 174 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 175 actionBar.setDisplayShowCustomEnabled(true); 176 177 configureSearchMode(); 178 } 179 180 private void configureSearchMode() { 181 final ActionBar actionBar = getActionBar(); 182 if (mIsSearchMode) { 183 actionBar.setDisplayShowTitleEnabled(false); 184 mSearchViewContainer.setVisibility(View.VISIBLE); 185 mSearchView.requestFocus(); 186 } else { 187 actionBar.setDisplayShowTitleEnabled(true); 188 mSearchViewContainer.setVisibility(View.GONE); 189 mSearchView.setQuery(null, true); 190 } 191 invalidateOptionsMenu(); 192 } 193 194 @Override 195 public boolean onOptionsItemSelected(MenuItem item) { 196 switch (item.getItemId()) { 197 case android.R.id.home: 198 // Go back to previous screen, intending "cancel" 199 setResult(RESULT_CANCELED); 200 onBackPressed(); 201 return true; 202 case R.id.menu_search: 203 mIsSearchMode = !mIsSearchMode; 204 configureSearchMode(); 205 return true; 206 } 207 return super.onOptionsItemSelected(item); 208 } 209 210 @Override 211 protected void onSaveInstanceState(Bundle outState) { 212 super.onSaveInstanceState(outState); 213 outState.putInt(KEY_ACTION_CODE, mActionCode); 214 outState.putBoolean(KEY_SEARCH_MODE, mIsSearchMode); 215 } 216 217 private void configureActivityTitle() { 218 if (!TextUtils.isEmpty(mRequest.getActivityTitle())) { 219 setTitle(mRequest.getActivityTitle()); 220 return; 221 } 222 223 int actionCode = mRequest.getActionCode(); 224 switch (actionCode) { 225 case ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT: { 226 setTitle(R.string.contactPickerActivityTitle); 227 break; 228 } 229 230 case ContactsRequest.ACTION_PICK_CONTACT: { 231 setTitle(R.string.contactPickerActivityTitle); 232 break; 233 } 234 235 case ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT: { 236 setTitle(R.string.contactPickerActivityTitle); 237 break; 238 } 239 240 case ContactsRequest.ACTION_CREATE_SHORTCUT_CONTACT: { 241 setTitle(R.string.shortcutActivityTitle); 242 break; 243 } 244 245 case ContactsRequest.ACTION_PICK_PHONE: { 246 setTitle(R.string.contactPickerActivityTitle); 247 break; 248 } 249 250 case ContactsRequest.ACTION_PICK_EMAIL: { 251 setTitle(R.string.contactPickerActivityTitle); 252 break; 253 } 254 255 case ContactsRequest.ACTION_CREATE_SHORTCUT_CALL: { 256 setTitle(R.string.callShortcutActivityTitle); 257 break; 258 } 259 260 case ContactsRequest.ACTION_CREATE_SHORTCUT_SMS: { 261 setTitle(R.string.messageShortcutActivityTitle); 262 break; 263 } 264 265 case ContactsRequest.ACTION_PICK_POSTAL: { 266 setTitle(R.string.contactPickerActivityTitle); 267 break; 268 } 269 270 case ContactsRequest.ACTION_PICK_JOIN: { 271 setTitle(R.string.titleJoinContactDataWith); 272 break; 273 } 274 } 275 } 276 277 /** 278 * Creates the fragment based on the current request. 279 */ 280 public void configureListFragment() { 281 switch (mActionCode) { 282 case ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT: { 283 ContactPickerFragment fragment = new ContactPickerFragment(); 284 fragment.setEditMode(true); 285 fragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); 286 fragment.setCreateContactEnabled(!mRequest.isSearchMode()); 287 mListFragment = fragment; 288 break; 289 } 290 291 case ContactsRequest.ACTION_DEFAULT: 292 case ContactsRequest.ACTION_PICK_CONTACT: { 293 ContactPickerFragment fragment = new ContactPickerFragment(); 294 fragment.setIncludeProfile(mRequest.shouldIncludeProfile()); 295 mListFragment = fragment; 296 break; 297 } 298 299 case ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT: { 300 ContactPickerFragment fragment = new ContactPickerFragment(); 301 fragment.setCreateContactEnabled(!mRequest.isSearchMode()); 302 mListFragment = fragment; 303 break; 304 } 305 306 case ContactsRequest.ACTION_CREATE_SHORTCUT_CONTACT: { 307 ContactPickerFragment fragment = new ContactPickerFragment(); 308 fragment.setShortcutRequested(true); 309 mListFragment = fragment; 310 break; 311 } 312 313 case ContactsRequest.ACTION_PICK_PHONE: { 314 PhoneNumberPickerFragment fragment = getPhoneNumberPickerFragment(mRequest); 315 mListFragment = fragment; 316 break; 317 } 318 319 case ContactsRequest.ACTION_PICK_EMAIL: { 320 mListFragment = new EmailAddressPickerFragment(); 321 break; 322 } 323 324 case ContactsRequest.ACTION_CREATE_SHORTCUT_CALL: { 325 PhoneNumberPickerFragment fragment = getPhoneNumberPickerFragment(mRequest); 326 fragment.setShortcutAction(Intent.ACTION_CALL); 327 328 mListFragment = fragment; 329 break; 330 } 331 332 case ContactsRequest.ACTION_CREATE_SHORTCUT_SMS: { 333 PhoneNumberPickerFragment fragment = getPhoneNumberPickerFragment(mRequest); 334 fragment.setShortcutAction(Intent.ACTION_SENDTO); 335 336 mListFragment = fragment; 337 break; 338 } 339 340 case ContactsRequest.ACTION_PICK_POSTAL: { 341 PostalAddressPickerFragment fragment = new PostalAddressPickerFragment(); 342 343 mListFragment = fragment; 344 break; 345 } 346 347 case ContactsRequest.ACTION_PICK_JOIN: { 348 JoinContactListFragment joinFragment = new JoinContactListFragment(); 349 joinFragment.setTargetContactId(getTargetContactId()); 350 mListFragment = joinFragment; 351 break; 352 } 353 354 default: 355 throw new IllegalStateException("Invalid action code: " + mActionCode); 356 } 357 358 // Setting compatibility is no longer needed for PhoneNumberPickerFragment since that logic 359 // has been separated into LegacyPhoneNumberPickerFragment. But we still need to set 360 // compatibility for other fragments. 361 mListFragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode()); 362 mListFragment.setDirectoryResultLimit(DEFAULT_DIRECTORY_RESULT_LIMIT); 363 364 getFragmentManager().beginTransaction() 365 .replace(R.id.list_container, mListFragment) 366 .commitAllowingStateLoss(); 367 } 368 369 private PhoneNumberPickerFragment getPhoneNumberPickerFragment(ContactsRequest request) { 370 if (mRequest.isLegacyCompatibilityMode()) { 371 return new LegacyPhoneNumberPickerFragment(); 372 } else { 373 return new PhoneNumberPickerFragment(); 374 } 375 } 376 377 public void setupActionListener() { 378 if (mListFragment instanceof ContactPickerFragment) { 379 ((ContactPickerFragment) mListFragment).setOnContactPickerActionListener( 380 new ContactPickerActionListener()); 381 } else if (mListFragment instanceof PhoneNumberPickerFragment) { 382 ((PhoneNumberPickerFragment) mListFragment).setOnPhoneNumberPickerActionListener( 383 new PhoneNumberPickerActionListener()); 384 } else if (mListFragment instanceof PostalAddressPickerFragment) { 385 ((PostalAddressPickerFragment) mListFragment).setOnPostalAddressPickerActionListener( 386 new PostalAddressPickerActionListener()); 387 } else if (mListFragment instanceof EmailAddressPickerFragment) { 388 ((EmailAddressPickerFragment) mListFragment).setOnEmailAddressPickerActionListener( 389 new EmailAddressPickerActionListener()); 390 } else if (mListFragment instanceof JoinContactListFragment) { 391 ((JoinContactListFragment) mListFragment).setOnContactPickerActionListener( 392 new JoinContactActionListener()); 393 } else { 394 throw new IllegalStateException("Unsupported list fragment type: " + mListFragment); 395 } 396 } 397 398 private final class ContactPickerActionListener implements OnContactPickerActionListener { 399 @Override 400 public void onCreateNewContactAction() { 401 startCreateNewContactActivity(); 402 } 403 404 @Override 405 public void onEditContactAction(Uri contactLookupUri) { 406 Bundle extras = getIntent().getExtras(); 407 if (launchAddToContactDialog(extras)) { 408 // Show a confirmation dialog to add the value(s) to the existing contact. 409 Intent intent = new Intent(ContactSelectionActivity.this, 410 ConfirmAddDetailActivity.class); 411 intent.setData(contactLookupUri); 412 if (extras != null) { 413 // First remove name key if present because the dialog does not support name 414 // editing. This is fine because the user wants to add information to an 415 // existing contact, who should already have a name and we wouldn't want to 416 // override the name. 417 extras.remove(Insert.NAME); 418 intent.putExtras(extras); 419 } 420 421 // Wait for the activity result because we want to keep the picker open (in case the 422 // user cancels adding the info to a contact and wants to pick someone else). 423 startActivityForResult(intent, SUBACTIVITY_ADD_TO_EXISTING_CONTACT); 424 } else { 425 // Otherwise launch the full contact editor. 426 startActivityAndForwardResult(new Intent(Intent.ACTION_EDIT, contactLookupUri)); 427 } 428 } 429 430 @Override 431 public void onPickContactAction(Uri contactUri) { 432 returnPickerResult(contactUri); 433 } 434 435 @Override 436 public void onShortcutIntentCreated(Intent intent) { 437 returnPickerResult(intent); 438 } 439 440 /** 441 * Returns true if is a single email or single phone number provided in the {@link Intent} 442 * extras bundle so that a pop-up confirmation dialog can be used to add the data to 443 * a contact. Otherwise return false if there are other intent extras that require launching 444 * the full contact editor. Ignore extras with the key {@link Insert.NAME} because names 445 * are a special case and we typically don't want to replace the name of an existing 446 * contact. 447 */ 448 private boolean launchAddToContactDialog(Bundle extras) { 449 if (extras == null) { 450 return false; 451 } 452 453 // Copy extras because the set may be modified in the next step 454 Set<String> intentExtraKeys = Sets.newHashSet(); 455 intentExtraKeys.addAll(extras.keySet()); 456 457 // Ignore name key because this is an existing contact. 458 if (intentExtraKeys.contains(Insert.NAME)) { 459 intentExtraKeys.remove(Insert.NAME); 460 } 461 462 int numIntentExtraKeys = intentExtraKeys.size(); 463 if (numIntentExtraKeys == 2) { 464 boolean hasPhone = intentExtraKeys.contains(Insert.PHONE) && 465 intentExtraKeys.contains(Insert.PHONE_TYPE); 466 boolean hasEmail = intentExtraKeys.contains(Insert.EMAIL) && 467 intentExtraKeys.contains(Insert.EMAIL_TYPE); 468 return hasPhone || hasEmail; 469 } else if (numIntentExtraKeys == 1) { 470 return intentExtraKeys.contains(Insert.PHONE) || 471 intentExtraKeys.contains(Insert.EMAIL); 472 } 473 // Having 0 or more than 2 intent extra keys means that we should launch 474 // the full contact editor to properly handle the intent extras. 475 return false; 476 } 477 } 478 479 private final class PhoneNumberPickerActionListener implements 480 OnPhoneNumberPickerActionListener { 481 @Override 482 public void onPickPhoneNumberAction(Uri dataUri) { 483 returnPickerResult(dataUri); 484 } 485 486 @Override 487 public void onCallNumberDirectly(String phoneNumber) { 488 Log.w(TAG, "Unsupported call."); 489 } 490 491 @Override 492 public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall) { 493 Log.w(TAG, "Unsupported call."); 494 } 495 496 @Override 497 public void onShortcutIntentCreated(Intent intent) { 498 returnPickerResult(intent); 499 } 500 501 public void onHomeInActionBarSelected() { 502 ContactSelectionActivity.this.onBackPressed(); 503 } 504 } 505 506 private final class JoinContactActionListener implements OnContactPickerActionListener { 507 @Override 508 public void onPickContactAction(Uri contactUri) { 509 Intent intent = new Intent(null, contactUri); 510 setResult(RESULT_OK, intent); 511 finish(); 512 } 513 514 @Override 515 public void onShortcutIntentCreated(Intent intent) { 516 } 517 518 @Override 519 public void onCreateNewContactAction() { 520 } 521 522 @Override 523 public void onEditContactAction(Uri contactLookupUri) { 524 } 525 } 526 527 private final class PostalAddressPickerActionListener implements 528 OnPostalAddressPickerActionListener { 529 @Override 530 public void onPickPostalAddressAction(Uri dataUri) { 531 returnPickerResult(dataUri); 532 } 533 } 534 535 private final class EmailAddressPickerActionListener implements 536 OnEmailAddressPickerActionListener { 537 @Override 538 public void onPickEmailAddressAction(Uri dataUri) { 539 returnPickerResult(dataUri); 540 } 541 } 542 543 public void startActivityAndForwardResult(final Intent intent) { 544 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 545 546 // Forward extras to the new activity 547 Bundle extras = getIntent().getExtras(); 548 if (extras != null) { 549 intent.putExtras(extras); 550 } 551 try { 552 startActivity(intent); 553 } catch (ActivityNotFoundException e) { 554 Log.e(TAG, "startActivity() failed: " + e); 555 Toast.makeText(ContactSelectionActivity.this, R.string.missing_app, 556 Toast.LENGTH_SHORT).show(); 557 } 558 finish(); 559 } 560 561 @Override 562 public boolean onQueryTextChange(String newText) { 563 mListFragment.setQueryString(newText, true); 564 return false; 565 } 566 567 @Override 568 public boolean onQueryTextSubmit(String query) { 569 return false; 570 } 571 572 @Override 573 public boolean onClose() { 574 if (!TextUtils.isEmpty(mSearchView.getQuery())) { 575 mSearchView.setQuery(null, true); 576 } 577 return true; 578 } 579 580 @Override 581 public void onFocusChange(View view, boolean hasFocus) { 582 switch (view.getId()) { 583 case R.id.search_view: { 584 if (hasFocus) { 585 showInputMethod(mSearchView.findFocus()); 586 } 587 } 588 } 589 } 590 591 public void returnPickerResult(Uri data) { 592 Intent intent = new Intent(); 593 intent.setData(data); 594 returnPickerResult(intent); 595 } 596 597 public void returnPickerResult(Intent intent) { 598 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 599 setResult(RESULT_OK, intent); 600 finish(); 601 } 602 603 @Override 604 public void onClick(View view) { 605 switch (view.getId()) { 606 case R.id.floating_action_button: { 607 startCreateNewContactActivity(); 608 break; 609 } 610 } 611 } 612 613 private long getTargetContactId() { 614 Intent intent = getIntent(); 615 final long targetContactId = intent.getLongExtra(UI.TARGET_CONTACT_ID_EXTRA_KEY, -1); 616 if (targetContactId == -1) { 617 Log.e(TAG, "Intent " + intent.getAction() + " is missing required extra: " 618 + UI.TARGET_CONTACT_ID_EXTRA_KEY); 619 setResult(RESULT_CANCELED); 620 finish(); 621 return -1; 622 } 623 return targetContactId; 624 } 625 626 private void startCreateNewContactActivity() { 627 Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 628 intent.putExtra(ContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, true); 629 startActivityAndForwardResult(intent); 630 } 631 632 private void showInputMethod(View view) { 633 final InputMethodManager imm = (InputMethodManager) 634 getSystemService(Context.INPUT_METHOD_SERVICE); 635 if (imm != null) { 636 if (!imm.showSoftInput(view, 0)) { 637 Log.w(TAG, "Failed to show soft input method."); 638 } 639 } 640 } 641 642 @Override 643 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 644 super.onActivityResult(requestCode, resultCode, data); 645 if (requestCode == SUBACTIVITY_ADD_TO_EXISTING_CONTACT) { 646 if (resultCode == Activity.RESULT_OK) { 647 if (data != null) { 648 startActivity(data); 649 } 650 finish(); 651 } 652 } 653 } 654 655 @Override 656 public boolean onCreateOptionsMenu(Menu menu) { 657 super.onCreateOptionsMenu(menu); 658 659 final MenuInflater inflater = getMenuInflater(); 660 inflater.inflate(R.menu.search_menu, menu); 661 662 final MenuItem searchItem = menu.findItem(R.id.menu_search); 663 searchItem.setVisible(!mIsSearchMode); 664 return true; 665 } 666 667 @Override 668 public void onBackPressed() { 669 if (mIsSearchMode) { 670 mIsSearchMode = false; 671 configureSearchMode(); 672 } else { 673 super.onBackPressed(); 674 } 675 } 676} 677