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