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