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