DialtactsActivity.java revision bf7a95e47facece917e99f19848c014c1438cd77
1/* 2 * Copyright (C) 2013 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.dialer; 18 19import android.animation.LayoutTransition; 20import android.app.ActionBar; 21import android.app.Activity; 22import android.app.Fragment; 23import android.app.FragmentTransaction; 24import android.content.ActivityNotFoundException; 25import android.content.Context; 26import android.content.Intent; 27import android.content.pm.PackageManager; 28import android.content.pm.ResolveInfo; 29import android.content.res.Configuration; 30import android.content.res.Resources; 31import android.net.Uri; 32import android.os.Bundle; 33import android.os.RemoteException; 34import android.phone.PhoneManager; 35import android.provider.ContactsContract.Contacts; 36import android.provider.ContactsContract.Intents; 37import android.speech.RecognizerIntent; 38import android.support.v4.view.ViewPager; 39import android.telecomm.TelecommManager; 40import android.telephony.TelephonyManager; 41import android.text.Editable; 42import android.text.TextUtils; 43import android.text.TextWatcher; 44import android.util.Log; 45import android.view.DragEvent; 46import android.view.Gravity; 47import android.view.KeyEvent; 48import android.view.Menu; 49import android.view.MenuItem; 50import android.view.MotionEvent; 51import android.view.View; 52import android.view.View.OnDragListener; 53import android.view.View.OnTouchListener; 54import android.view.ViewTreeObserver; 55import android.view.animation.Animation; 56import android.view.animation.AnimationUtils; 57import android.view.inputmethod.InputMethodManager; 58import android.widget.AbsListView.OnScrollListener; 59import android.widget.EditText; 60import android.widget.FrameLayout; 61import android.widget.ImageButton; 62import android.widget.PopupMenu; 63import android.widget.Toast; 64 65import com.android.contacts.common.CallUtil; 66import com.android.contacts.common.dialog.ClearFrequentsDialog; 67import com.android.contacts.common.interactions.ImportExportDialogFragment; 68import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; 69import com.android.contacts.common.widget.FloatingActionButtonController; 70import com.android.dialer.activity.TransactionSafeActivity; 71import com.android.dialer.calllog.CallLogActivity; 72import com.android.dialer.database.DialerDatabaseHelper; 73import com.android.dialer.dialpad.DialpadFragment; 74import com.android.dialer.dialpad.SmartDialNameMatcher; 75import com.android.dialer.dialpad.SmartDialPrefix; 76import com.android.dialer.interactions.PhoneNumberInteraction; 77import com.android.dialer.list.DragDropController; 78import com.android.dialer.list.ListsFragment; 79import com.android.dialer.list.OnDragDropListener; 80import com.android.dialer.list.OnListFragmentScrolledListener; 81import com.android.dialer.list.PhoneFavoriteSquareTileView; 82import com.android.dialer.list.RegularSearchFragment; 83import com.android.dialer.list.SearchFragment; 84import com.android.dialer.list.SmartDialSearchFragment; 85import com.android.dialer.list.SpeedDialFragment; 86import com.android.dialer.settings.DialerSettingsActivity; 87import com.android.dialer.util.DialerUtils; 88import com.android.dialer.widget.ActionBarController; 89import com.android.dialer.widget.SearchEditTextLayout; 90import com.android.dialer.widget.SearchEditTextLayout.OnBackButtonClickedListener; 91import com.android.dialerbind.DatabaseHelperManager; 92import com.android.phone.common.animation.AnimUtils; 93import com.android.phone.common.animation.AnimationListenerAdapter; 94 95import java.util.ArrayList; 96import java.util.List; 97import java.util.Locale; 98 99/** 100 * The dialer tab's title is 'phone', a more common name (see strings.xml). 101 */ 102public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener, 103 DialpadFragment.OnDialpadQueryChangedListener, 104 OnListFragmentScrolledListener, 105 DialpadFragment.HostInterface, 106 ListsFragment.HostInterface, 107 SpeedDialFragment.HostInterface, 108 SearchFragment.HostInterface, 109 OnDragDropListener, 110 OnPhoneNumberPickerActionListener, 111 PopupMenu.OnMenuItemClickListener, 112 ViewPager.OnPageChangeListener, 113 ActionBarController.ActivityUi { 114 private static final String TAG = "DialtactsActivity"; 115 116 public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 117 118 public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences"; 119 120 /** @see #getCallOrigin() */ 121 private static final String CALL_ORIGIN_DIALTACTS = 122 "com.android.dialer.DialtactsActivity"; 123 124 private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui"; 125 private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui"; 126 private static final String KEY_SEARCH_QUERY = "search_query"; 127 private static final String KEY_FIRST_LAUNCH = "first_launch"; 128 private static final String KEY_IS_DIALPAD_SHOWN = "is_dialpad_shown"; 129 130 private static final String TAG_DIALPAD_FRAGMENT = "dialpad"; 131 private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search"; 132 private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial"; 133 private static final String TAG_FAVORITES_FRAGMENT = "favorites"; 134 135 /** 136 * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}. 137 */ 138 private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER"; 139 140 private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1; 141 142 private FrameLayout parentLayout; 143 144 /** 145 * Fragment containing the dialpad that slides into view 146 */ 147 private DialpadFragment mDialpadFragment; 148 149 /** 150 * Fragment for searching phone numbers using the alphanumeric keyboard. 151 */ 152 private RegularSearchFragment mRegularSearchFragment; 153 154 /** 155 * Fragment for searching phone numbers using the dialpad. 156 */ 157 private SmartDialSearchFragment mSmartDialSearchFragment; 158 159 /** 160 * Animation that slides in. 161 */ 162 private Animation mSlideIn; 163 164 /** 165 * Animation that slides out. 166 */ 167 private Animation mSlideOut; 168 169 /** 170 * Listener for after slide out animation completes on dialer fragment. 171 */ 172 AnimationListenerAdapter mSlideOutListener = new AnimationListenerAdapter() { 173 @Override 174 public void onAnimationEnd(Animation animation) { 175 commitDialpadFragmentHide(); 176 } 177 }; 178 179 /** 180 * Fragment containing the speed dial list, recents list, and all contacts list. 181 */ 182 private ListsFragment mListsFragment; 183 184 private boolean mInDialpadSearch; 185 private boolean mInRegularSearch; 186 private boolean mClearSearchOnPause; 187 private boolean mIsDialpadShown; 188 private boolean mShowDialpadOnResume; 189 190 /** 191 * Whether or not the device is in landscape orientation. 192 */ 193 private boolean mIsLandscape; 194 195 /** 196 * The position of the currently selected tab in the attached {@link ListsFragment}. 197 */ 198 private int mCurrentTabPosition = 0; 199 200 /** 201 * True if the dialpad is only temporarily showing due to being in call 202 */ 203 private boolean mInCallDialpadUp; 204 205 /** 206 * True when this activity has been launched for the first time. 207 */ 208 private boolean mFirstLaunch; 209 210 /** 211 * Search query to be applied to the SearchView in the ActionBar once 212 * onCreateOptionsMenu has been called. 213 */ 214 private String mPendingSearchViewQuery; 215 216 private PopupMenu mOverflowMenu; 217 private EditText mSearchView; 218 private View mVoiceSearchButton; 219 220 private String mSearchQuery; 221 222 private DialerDatabaseHelper mDialerDatabaseHelper; 223 private DragDropController mDragDropController; 224 private ActionBarController mActionBarController; 225 226 private String mDescriptionDialButtonStr; 227 private String mActionMenuDialpadButtonStr; 228 private ImageButton mFloatingActionButton; 229 private FloatingActionButtonController mFloatingActionButtonController; 230 /** 231 * Additional offset for FAB to be lowered when dialpad is open. 232 */ 233 private int mFloatingActionButtonDialpadMarginBottomOffset; 234 235 private int mActionBarHeight; 236 237 private class OptionsPopupMenu extends PopupMenu { 238 public OptionsPopupMenu(Context context, View anchor) { 239 super(context, anchor, Gravity.END); 240 } 241 242 @Override 243 public void show() { 244 final Menu menu = getMenu(); 245 final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents); 246 clearFrequents.setVisible(mListsFragment != null && 247 mListsFragment.getSpeedDialFragment() != null && 248 mListsFragment.getSpeedDialFragment().hasFrequents()); 249 super.show(); 250 } 251 } 252 253 /** 254 * Listener that listens to drag events and sends their x and y coordinates to a 255 * {@link DragDropController}. 256 */ 257 private class LayoutOnDragListener implements OnDragListener { 258 @Override 259 public boolean onDrag(View v, DragEvent event) { 260 if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) { 261 mDragDropController.handleDragHovered(v, (int) event.getX(), (int) event.getY()); 262 } 263 return true; 264 } 265 } 266 267 /** 268 * Listener used to send search queries to the phone search fragment. 269 */ 270 private final TextWatcher mPhoneSearchQueryTextListener = new TextWatcher() { 271 @Override 272 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 273 } 274 275 @Override 276 public void onTextChanged(CharSequence s, int start, int before, int count) { 277 final String newText = s.toString(); 278 if (newText.equals(mSearchQuery)) { 279 // If the query hasn't changed (perhaps due to activity being destroyed 280 // and restored, or user launching the same DIAL intent twice), then there is 281 // no need to do anything here. 282 return; 283 } 284 if (DEBUG) { 285 Log.d(TAG, "onTextChange for mSearchView called with new query: " + newText); 286 Log.d(TAG, "Previous Query: " + mSearchQuery); 287 } 288 mSearchQuery = newText; 289 290 // Show search fragment only when the query string is changed to non-empty text. 291 if (!TextUtils.isEmpty(newText)) { 292 // Call enterSearchUi only if we are switching search modes, or showing a search 293 // fragment for the first time. 294 final boolean sameSearchMode = (mIsDialpadShown && mInDialpadSearch) || 295 (!mIsDialpadShown && mInRegularSearch); 296 if (!sameSearchMode) { 297 enterSearchUi(mIsDialpadShown, mSearchQuery); 298 } 299 } 300 301 if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { 302 mSmartDialSearchFragment.setQueryString(mSearchQuery, false /* delaySelection */); 303 } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { 304 mRegularSearchFragment.setQueryString(mSearchQuery, false /* delaySelection */); 305 } 306 } 307 308 @Override 309 public void afterTextChanged(Editable s) { 310 } 311 }; 312 313 314 /** 315 * Open the search UI when the user clicks on the search box. 316 */ 317 private final View.OnClickListener mSearchViewOnClickListener = new View.OnClickListener() { 318 @Override 319 public void onClick(View v) { 320 if (!isInSearchUi()) { 321 mActionBarController.onSearchBoxTapped(); 322 enterSearchUi(false /* smartDialSearch */, mSearchView.getText().toString()); 323 } 324 } 325 }; 326 327 /** 328 * If the search term is empty and the user closes the soft keyboard, close the search UI. 329 */ 330 private final View.OnKeyListener mSearchEditTextLayoutListener = new View.OnKeyListener() { 331 @Override 332 public boolean onKey(View v, int keyCode, KeyEvent event) { 333 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN && 334 TextUtils.isEmpty(mSearchView.getText().toString())) { 335 maybeExitSearchUi(); 336 } 337 return false; 338 } 339 }; 340 341 @Override 342 protected void onCreate(Bundle savedInstanceState) { 343 super.onCreate(savedInstanceState); 344 mFirstLaunch = true; 345 346 final Resources resources = getResources(); 347 mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height_large); 348 mDescriptionDialButtonStr = resources.getString(R.string.description_dial_button); 349 mActionMenuDialpadButtonStr = resources.getString(R.string.action_menu_dialpad_button); 350 351 setContentView(R.layout.dialtacts_activity); 352 getWindow().setBackgroundDrawable(null); 353 354 final ActionBar actionBar = getActionBar(); 355 actionBar.setCustomView(R.layout.search_edittext); 356 actionBar.setDisplayShowCustomEnabled(true); 357 actionBar.setBackgroundDrawable(null); 358 actionBar.setElevation(resources.getDimensionPixelSize(R.dimen.action_bar_elevation)); 359 360 mActionBarController = new ActionBarController(this, 361 (SearchEditTextLayout) actionBar.getCustomView()); 362 363 SearchEditTextLayout searchEditTextLayout = 364 (SearchEditTextLayout) actionBar.getCustomView(); 365 searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener); 366 367 mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view); 368 mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); 369 mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button); 370 searchEditTextLayout.findViewById(R.id.search_magnifying_glass) 371 .setOnClickListener(mSearchViewOnClickListener); 372 searchEditTextLayout.findViewById(R.id.search_box_start_search) 373 .setOnClickListener(mSearchViewOnClickListener); 374 searchEditTextLayout.setOnBackButtonClickedListener(new OnBackButtonClickedListener() { 375 @Override 376 public void onBackButtonClicked() { 377 onBackPressed(); 378 } 379 }); 380 381 mIsLandscape = getResources().getConfiguration().orientation 382 == Configuration.ORIENTATION_LANDSCAPE; 383 384 final View floatingActionButtonContainer = findViewById( 385 R.id.floating_action_button_container); 386 mFloatingActionButton = (ImageButton) findViewById(R.id.floating_action_button); 387 int floatingActionButtonWidth = resources.getDimensionPixelSize( 388 R.dimen.floating_action_button_width); 389 mFloatingActionButton.setOnClickListener(this); 390 mFloatingActionButtonController = new FloatingActionButtonController(this, 391 floatingActionButtonContainer); 392 mFloatingActionButtonDialpadMarginBottomOffset = resources.getDimensionPixelOffset( 393 R.dimen.floating_action_button_dialpad_margin_bottom_offset); 394 395 ImageButton optionsMenuButton = 396 (ImageButton) searchEditTextLayout.findViewById(R.id.dialtacts_options_menu_button); 397 optionsMenuButton.setOnClickListener(this); 398 mOverflowMenu = buildOptionsMenu(searchEditTextLayout); 399 optionsMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener()); 400 401 // Add the favorites fragment, and the dialpad fragment, but only if savedInstanceState 402 // is null. Otherwise the fragment manager takes care of recreating these fragments. 403 if (savedInstanceState == null) { 404 getFragmentManager().beginTransaction() 405 .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT) 406 .add(R.id.dialtacts_container, new DialpadFragment(), TAG_DIALPAD_FRAGMENT) 407 .commit(); 408 } else { 409 mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY); 410 mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI); 411 mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI); 412 mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH); 413 mShowDialpadOnResume = savedInstanceState.getBoolean(KEY_IS_DIALPAD_SHOWN); 414 mActionBarController.restoreInstanceState(savedInstanceState); 415 } 416 417 final boolean isLayoutRtl = DialerUtils.isRtl(); 418 if (mIsLandscape) { 419 mSlideIn = AnimationUtils.loadAnimation(this, 420 isLayoutRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right); 421 mSlideOut = AnimationUtils.loadAnimation(this, 422 isLayoutRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right); 423 } else { 424 mSlideIn = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom); 425 mSlideOut = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom); 426 } 427 428 mSlideIn.setInterpolator(AnimUtils.EASE_IN); 429 mSlideOut.setInterpolator(AnimUtils.EASE_OUT); 430 431 mSlideOut.setAnimationListener(mSlideOutListener); 432 433 parentLayout = (FrameLayout) findViewById(R.id.dialtacts_mainlayout); 434 parentLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); 435 parentLayout.setOnDragListener(new LayoutOnDragListener()); 436 floatingActionButtonContainer.getViewTreeObserver().addOnGlobalLayoutListener( 437 new ViewTreeObserver.OnGlobalLayoutListener() { 438 @Override 439 public void onGlobalLayout() { 440 final ViewTreeObserver observer = floatingActionButtonContainer 441 .getViewTreeObserver(); 442 if (!observer.isAlive()) { 443 return; 444 } 445 observer.removeOnGlobalLayoutListener(this); 446 int screenWidth = parentLayout.getWidth(); 447 mFloatingActionButtonController.setScreenWidth(screenWidth); 448 updateFloatingActionButtonControllerAlignment(false /* animate */); 449 } 450 }); 451 452 setupActivityOverlay(); 453 454 mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this); 455 SmartDialPrefix.initializeNanpSettings(this); 456 } 457 458 private void setupActivityOverlay() { 459 final View activityOverlay = findViewById(R.id.activity_overlay); 460 activityOverlay.setOnTouchListener(new OnTouchListener() { 461 @Override 462 public boolean onTouch(View v, MotionEvent event) { 463 if (!mIsDialpadShown) { 464 maybeExitSearchUi(); 465 } 466 return false; 467 } 468 }); 469 } 470 471 @Override 472 protected void onResume() { 473 super.onResume(); 474 if (mFirstLaunch) { 475 displayFragment(getIntent()); 476 } else if (!phoneIsInUse() && mInCallDialpadUp) { 477 hideDialpadFragment(false, true); 478 mInCallDialpadUp = false; 479 } else if (mShowDialpadOnResume) { 480 showDialpadFragment(false); 481 mShowDialpadOnResume = false; 482 } 483 mFirstLaunch = false; 484 prepareVoiceSearchButton(); 485 mDialerDatabaseHelper.startSmartDialUpdateThread(); 486 updateFloatingActionButtonControllerAlignment(false /* animate */); 487 } 488 489 @Override 490 protected void onPause() { 491 if (mClearSearchOnPause) { 492 hideDialpadAndSearchUi(); 493 mClearSearchOnPause = false; 494 } 495 super.onPause(); 496 } 497 498 @Override 499 protected void onSaveInstanceState(Bundle outState) { 500 super.onSaveInstanceState(outState); 501 outState.putString(KEY_SEARCH_QUERY, mSearchQuery); 502 outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch); 503 outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch); 504 outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch); 505 outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown); 506 mActionBarController.saveInstanceState(outState); 507 } 508 509 @Override 510 public void onAttachFragment(Fragment fragment) { 511 if (fragment instanceof DialpadFragment) { 512 mDialpadFragment = (DialpadFragment) fragment; 513 if (!mShowDialpadOnResume) { 514 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 515 transaction.hide(mDialpadFragment); 516 transaction.commit(); 517 } 518 } else if (fragment instanceof SmartDialSearchFragment) { 519 mSmartDialSearchFragment = (SmartDialSearchFragment) fragment; 520 mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this); 521 } else if (fragment instanceof SearchFragment) { 522 mRegularSearchFragment = (RegularSearchFragment) fragment; 523 mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this); 524 } else if (fragment instanceof ListsFragment) { 525 mListsFragment = (ListsFragment) fragment; 526 mListsFragment.addOnPageChangeListener(this); 527 } 528 } 529 530 protected void handleMenuSettings() { 531 final Intent intent = new Intent(this, DialerSettingsActivity.class); 532 startActivity(intent); 533 } 534 535 @Override 536 public void onClick(View view) { 537 switch (view.getId()) { 538 case R.id.floating_action_button: 539 if (!mIsDialpadShown) { 540 mInCallDialpadUp = false; 541 showDialpadFragment(true); 542 } else { 543 // Dial button was pressed; tell the Dialpad fragment 544 mDialpadFragment.dialButtonPressed(); 545 } 546 break; 547 case R.id.voice_search_button: 548 try { 549 startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 550 ACTIVITY_REQUEST_CODE_VOICE_SEARCH); 551 } catch (ActivityNotFoundException e) { 552 Toast.makeText(DialtactsActivity.this, R.string.voice_search_not_available, 553 Toast.LENGTH_SHORT).show(); 554 } 555 break; 556 case R.id.dialtacts_options_menu_button: 557 mOverflowMenu.show(); 558 break; 559 default: { 560 Log.wtf(TAG, "Unexpected onClick event from " + view); 561 break; 562 } 563 } 564 } 565 566 @Override 567 public boolean onMenuItemClick(MenuItem item) { 568 switch (item.getItemId()) { 569 case R.id.menu_history: 570 showCallHistory(); 571 break; 572 case R.id.menu_add_contact: 573 try { 574 startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI)); 575 } catch (ActivityNotFoundException e) { 576 Toast toast = Toast.makeText(this, 577 R.string.add_contact_not_available, 578 Toast.LENGTH_SHORT); 579 toast.show(); 580 } 581 break; 582 case R.id.menu_import_export: 583 // We hard-code the "contactsAreAvailable" argument because doing it properly would 584 // involve querying a {@link ProviderStatusLoader}, which we don't want to do right 585 // now in Dialtacts for (potential) performance reasons. Compare with how it is 586 // done in {@link PeopleActivity}. 587 ImportExportDialogFragment.show(getFragmentManager(), true, 588 DialtactsActivity.class); 589 return true; 590 case R.id.menu_clear_frequents: 591 ClearFrequentsDialog.show(getFragmentManager()); 592 return true; 593 case R.id.menu_call_settings: 594 handleMenuSettings(); 595 return true; 596 } 597 return false; 598 } 599 600 @Override 601 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 602 if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) { 603 if (resultCode == RESULT_OK) { 604 final ArrayList<String> matches = data.getStringArrayListExtra( 605 RecognizerIntent.EXTRA_RESULTS); 606 if (matches.size() > 0) { 607 final String match = matches.get(0); 608 mSearchView.setText(match); 609 } else { 610 Log.e(TAG, "Voice search - nothing heard"); 611 } 612 } else { 613 Log.e(TAG, "Voice search failed"); 614 } 615 } 616 super.onActivityResult(requestCode, resultCode, data); 617 } 618 619 /** 620 * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual 621 * updates are handled by a callback which is invoked after the dialpad fragment is shown. 622 * @see #onDialpadShown 623 */ 624 private void showDialpadFragment(boolean animate) { 625 if (mIsDialpadShown) { 626 return; 627 } 628 mIsDialpadShown = true; 629 mDialpadFragment.setAnimate(animate); 630 mDialpadFragment.sendScreenView(); 631 632 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 633 ft.show(mDialpadFragment); 634 ft.commit(); 635 636 mActionBarController.onDialpadUp(); 637 638 if (!isInSearchUi()) { 639 enterSearchUi(true /* isSmartDial */, mSearchQuery); 640 } 641 } 642 643 /** 644 * Callback from child DialpadFragment when the dialpad is shown. 645 */ 646 public void onDialpadShown() { 647 mFloatingActionButton.setImageResource(R.drawable.fab_ic_call); 648 mFloatingActionButton.setContentDescription(mDescriptionDialButtonStr); 649 updateFloatingActionButtonControllerAlignment(mDialpadFragment.getAnimate()); 650 if (mDialpadFragment.getAnimate()) { 651 mDialpadFragment.getView().startAnimation(mSlideIn); 652 } else { 653 mDialpadFragment.setYFraction(0); 654 } 655 656 updateSearchFragmentPosition(); 657 } 658 659 /** 660 * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in 661 * a callback after the hide animation ends. 662 * @see #commitDialpadFragmentHide 663 */ 664 public void hideDialpadFragment(boolean animate, boolean clearDialpad) { 665 if (mDialpadFragment == null) { 666 return; 667 } 668 if (clearDialpad) { 669 mDialpadFragment.clearDialpad(); 670 } 671 if (!mIsDialpadShown) { 672 return; 673 } 674 mIsDialpadShown = false; 675 mDialpadFragment.setAnimate(animate); 676 677 updateSearchFragmentPosition(); 678 mFloatingActionButton.setImageResource(R.drawable.fab_ic_dial); 679 mFloatingActionButton.setContentDescription(mActionMenuDialpadButtonStr); 680 681 updateFloatingActionButtonControllerAlignment(animate); 682 if (animate) { 683 mDialpadFragment.getView().startAnimation(mSlideOut); 684 } else { 685 commitDialpadFragmentHide(); 686 } 687 688 mActionBarController.onDialpadDown(); 689 690 if (isInSearchUi()) { 691 if (TextUtils.isEmpty(mSearchQuery)) { 692 exitSearchUi(); 693 } 694 } 695 } 696 697 /** 698 * Finishes hiding the dialpad fragment after any animations are completed. 699 */ 700 private void commitDialpadFragmentHide() { 701 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 702 ft.hide(mDialpadFragment); 703 ft.commit(); 704 } 705 706 private void updateSearchFragmentPosition() { 707 SearchFragment fragment = null; 708 if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { 709 fragment = mSmartDialSearchFragment; 710 } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { 711 fragment = mRegularSearchFragment; 712 } 713 if (fragment != null && fragment.isVisible()) { 714 fragment.updatePosition(true /* animate */); 715 } 716 } 717 718 @Override 719 public boolean isInSearchUi() { 720 return mInDialpadSearch || mInRegularSearch; 721 } 722 723 @Override 724 public boolean hasSearchQuery() { 725 return !TextUtils.isEmpty(mSearchQuery); 726 } 727 728 @Override 729 public boolean shouldShowActionBar() { 730 return mListsFragment.shouldShowActionBar(); 731 } 732 733 private void setNotInSearchUi() { 734 mInDialpadSearch = false; 735 mInRegularSearch = false; 736 } 737 738 private void hideDialpadAndSearchUi() { 739 if (mIsDialpadShown) { 740 hideDialpadFragment(false, true); 741 } else { 742 exitSearchUi(); 743 } 744 } 745 746 private void prepareVoiceSearchButton() { 747 final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 748 if (canIntentBeHandled(voiceIntent)) { 749 mVoiceSearchButton.setVisibility(View.VISIBLE); 750 mVoiceSearchButton.setOnClickListener(this); 751 } else { 752 mVoiceSearchButton.setVisibility(View.GONE); 753 } 754 } 755 756 private OptionsPopupMenu buildOptionsMenu(View invoker) { 757 final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker); 758 popupMenu.inflate(R.menu.dialtacts_options); 759 final Menu menu = popupMenu.getMenu(); 760 popupMenu.setOnMenuItemClickListener(this); 761 return popupMenu; 762 } 763 764 @Override 765 public boolean onCreateOptionsMenu(Menu menu) { 766 if (mPendingSearchViewQuery != null) { 767 mSearchView.setText(mPendingSearchViewQuery); 768 mPendingSearchViewQuery = null; 769 } 770 mActionBarController.restoreActionBarOffset(); 771 return false; 772 } 773 774 /** 775 * Returns true if the intent is due to hitting the green send key (hardware call button: 776 * KEYCODE_CALL) while in a call. 777 * 778 * @param intent the intent that launched this activity 779 * @return true if the intent is due to hitting the green send key while in a call 780 */ 781 private boolean isSendKeyWhileInCall(Intent intent) { 782 // If there is a call in progress and the user launched the dialer by hitting the call 783 // button, go straight to the in-call screen. 784 final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction()); 785 786 if (callKey) { 787 getPhoneManager().showCallScreen(false); 788 return true; 789 } 790 791 return false; 792 } 793 794 /** 795 * Sets the current tab based on the intent's request type 796 * 797 * @param intent Intent that contains information about which tab should be selected 798 */ 799 private void displayFragment(Intent intent) { 800 // If we got here by hitting send and we're in call forward along to the in-call activity 801 if (isSendKeyWhileInCall(intent)) { 802 finish(); 803 return; 804 } 805 806 if (mDialpadFragment != null) { 807 final boolean phoneIsInUse = phoneIsInUse(); 808 if (phoneIsInUse || (intent.getData() != null && isDialIntent(intent))) { 809 mDialpadFragment.setStartedFromNewIntent(true); 810 if (phoneIsInUse && !mDialpadFragment.isVisible()) { 811 mInCallDialpadUp = true; 812 } 813 showDialpadFragment(false); 814 } 815 } 816 } 817 818 @Override 819 public void onNewIntent(Intent newIntent) { 820 setIntent(newIntent); 821 displayFragment(newIntent); 822 823 invalidateOptionsMenu(); 824 } 825 826 /** Returns true if the given intent contains a phone number to populate the dialer with */ 827 private boolean isDialIntent(Intent intent) { 828 final String action = intent.getAction(); 829 if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) { 830 return true; 831 } 832 if (Intent.ACTION_VIEW.equals(action)) { 833 final Uri data = intent.getData(); 834 if (data != null && CallUtil.SCHEME_TEL.equals(data.getScheme())) { 835 return true; 836 } 837 } 838 return false; 839 } 840 841 /** 842 * Returns an appropriate call origin for this Activity. May return null when no call origin 843 * should be used (e.g. when some 3rd party application launched the screen. Call origin is 844 * for remembering the tab in which the user made a phone call, so the external app's DIAL 845 * request should not be counted.) 846 */ 847 public String getCallOrigin() { 848 return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null; 849 } 850 851 /** 852 * Shows the search fragment 853 */ 854 private void enterSearchUi(boolean smartDialSearch, String query) { 855 if (getFragmentManager().isDestroyed()) { 856 // Weird race condition where fragment is doing work after the activity is destroyed 857 // due to talkback being on (b/10209937). Just return since we can't do any 858 // constructive here. 859 return; 860 } 861 862 if (DEBUG) { 863 Log.d(TAG, "Entering search UI - smart dial " + smartDialSearch); 864 } 865 866 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 867 if (mInDialpadSearch && mSmartDialSearchFragment != null) { 868 transaction.remove(mSmartDialSearchFragment); 869 } else if (mInRegularSearch && mRegularSearchFragment != null) { 870 transaction.remove(mRegularSearchFragment); 871 } 872 873 final String tag; 874 if (smartDialSearch) { 875 tag = TAG_SMARTDIAL_SEARCH_FRAGMENT; 876 } else { 877 tag = TAG_REGULAR_SEARCH_FRAGMENT; 878 } 879 mInDialpadSearch = smartDialSearch; 880 mInRegularSearch = !smartDialSearch; 881 882 SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag); 883 transaction.setCustomAnimations(android.R.animator.fade_in, 0); 884 if (fragment == null) { 885 if (smartDialSearch) { 886 fragment = new SmartDialSearchFragment(); 887 } else { 888 fragment = new RegularSearchFragment(); 889 } 890 transaction.add(R.id.dialtacts_frame, fragment, tag); 891 } else { 892 transaction.show(fragment); 893 } 894 // DialtactsActivity will provide the options menu 895 fragment.setHasOptionsMenu(false); 896 fragment.setShowEmptyListForNullQuery(true); 897 fragment.setQueryString(query, false /* delaySelection */); 898 transaction.commit(); 899 900 mListsFragment.getView().animate().alpha(0).withLayer(); 901 } 902 903 /** 904 * Hides the search fragment 905 */ 906 private void exitSearchUi() { 907 // See related bug in enterSearchUI(); 908 if (getFragmentManager().isDestroyed()) { 909 return; 910 } 911 912 mSearchView.setText(null); 913 mDialpadFragment.clearDialpad(); 914 setNotInSearchUi(); 915 916 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 917 if (mSmartDialSearchFragment != null) { 918 transaction.remove(mSmartDialSearchFragment); 919 } 920 if (mRegularSearchFragment != null) { 921 transaction.remove(mRegularSearchFragment); 922 } 923 transaction.commit(); 924 925 mListsFragment.getView().animate().alpha(1).withLayer(); 926 mActionBarController.onSearchUiExited(); 927 } 928 929 /** Returns an Intent to launch Call Settings screen */ 930 public static Intent getCallSettingsIntent() { 931 final Intent intent = new Intent(TelecommManager.ACTION_SHOW_CALL_SETTINGS); 932 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 933 return intent; 934 } 935 936 @Override 937 public void onBackPressed() { 938 if (mIsDialpadShown) { 939 if (TextUtils.isEmpty(mSearchQuery) || 940 (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible() 941 && mSmartDialSearchFragment.getAdapter().getCount() == 0)) { 942 exitSearchUi(); 943 } 944 hideDialpadFragment(true, false); 945 } else if (isInSearchUi()) { 946 exitSearchUi(); 947 DialerUtils.hideInputMethod(parentLayout); 948 } else { 949 super.onBackPressed(); 950 } 951 } 952 953 /** 954 * @return True if the search UI was exited, false otherwise 955 */ 956 private boolean maybeExitSearchUi() { 957 if (isInSearchUi() && TextUtils.isEmpty(mSearchQuery)) { 958 exitSearchUi(); 959 DialerUtils.hideInputMethod(parentLayout); 960 return true; 961 } 962 return false; 963 } 964 965 @Override 966 public void onDialpadQueryChanged(String query) { 967 if (mSmartDialSearchFragment != null) { 968 mSmartDialSearchFragment.setAddToContactNumber(query); 969 } 970 final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query, 971 SmartDialNameMatcher.LATIN_SMART_DIAL_MAP); 972 973 if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) { 974 if (DEBUG) { 975 Log.d(TAG, "onDialpadQueryChanged - new query: " + query); 976 } 977 if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { 978 // This callback can happen if the dialpad fragment is recreated because of 979 // activity destruction. In that case, don't update the search view because 980 // that would bring the user back to the search fragment regardless of the 981 // previous state of the application. Instead, just return here and let the 982 // fragment manager correctly figure out whatever fragment was last displayed. 983 if (!TextUtils.isEmpty(normalizedQuery)) { 984 mPendingSearchViewQuery = normalizedQuery; 985 } 986 return; 987 } 988 mSearchView.setText(normalizedQuery); 989 } 990 } 991 992 @Override 993 public void onListFragmentScrollStateChange(int scrollState) { 994 if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 995 hideDialpadFragment(true, false); 996 DialerUtils.hideInputMethod(getCurrentFocus()); 997 } 998 } 999 1000 @Override 1001 public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, 1002 int totalItemCount) { 1003 // TODO: No-op for now. This should eventually show/hide the actionBar based on 1004 // interactions with the ListsFragments. 1005 } 1006 1007 @Override 1008 public void setFloatingActionButtonVisible(boolean visible) { 1009 mFloatingActionButtonController.setVisible(visible); 1010 } 1011 1012 private boolean phoneIsInUse() { 1013 return getPhoneManager().isInAPhoneCall(); 1014 } 1015 1016 public static Intent getAddNumberToContactIntent(CharSequence text) { 1017 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 1018 intent.putExtra(Intents.Insert.PHONE, text); 1019 intent.setType(Contacts.CONTENT_ITEM_TYPE); 1020 return intent; 1021 } 1022 1023 private boolean canIntentBeHandled(Intent intent) { 1024 final PackageManager packageManager = getPackageManager(); 1025 final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent, 1026 PackageManager.MATCH_DEFAULT_ONLY); 1027 return resolveInfo != null && resolveInfo.size() > 0; 1028 } 1029 1030 @Override 1031 public void showCallHistory() { 1032 // Use explicit CallLogActivity intent instead of ACTION_VIEW + 1033 // CONTENT_TYPE, so that we always open our call log from our dialer 1034 final Intent intent = new Intent(this, CallLogActivity.class); 1035 startActivity(intent); 1036 } 1037 1038 /** 1039 * Called when the user has long-pressed a contact tile to start a drag operation. 1040 */ 1041 @Override 1042 public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { 1043 if (mListsFragment.isPaneOpen()) { 1044 mActionBarController.setAlpha(ListsFragment.REMOVE_VIEW_SHOWN_ALPHA); 1045 } 1046 mListsFragment.showRemoveView(true); 1047 } 1048 1049 @Override 1050 public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) { 1051 } 1052 1053 /** 1054 * Called when the user has released a contact tile after long-pressing it. 1055 */ 1056 @Override 1057 public void onDragFinished(int x, int y) { 1058 if (mListsFragment.isPaneOpen()) { 1059 mActionBarController.setAlpha(ListsFragment.REMOVE_VIEW_HIDDEN_ALPHA); 1060 } 1061 mListsFragment.showRemoveView(false); 1062 } 1063 1064 @Override 1065 public void onDroppedOnRemove() {} 1066 1067 /** 1068 * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer 1069 * once it has been attached to the activity. 1070 */ 1071 @Override 1072 public void setDragDropController(DragDropController dragController) { 1073 mDragDropController = dragController; 1074 mListsFragment.getRemoveView().setDragDropController(dragController); 1075 } 1076 1077 @Override 1078 public void onPickPhoneNumberAction(Uri dataUri) { 1079 // Specify call-origin so that users will see the previous tab instead of 1080 // CallLog screen (search UI will be automatically exited). 1081 PhoneNumberInteraction.startInteractionForPhoneCall( 1082 DialtactsActivity.this, dataUri, getCallOrigin()); 1083 mClearSearchOnPause = true; 1084 } 1085 1086 @Override 1087 public void onCallNumberDirectly(String phoneNumber) { 1088 onCallNumberDirectly(phoneNumber, false /* isVideoCall */); 1089 } 1090 1091 @Override 1092 public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall) { 1093 Intent intent = isVideoCall ? 1094 CallUtil.getVideoCallIntent(phoneNumber, getCallOrigin()) : 1095 CallUtil.getCallIntent(phoneNumber, getCallOrigin()); 1096 DialerUtils.startActivityWithErrorToast(this, intent); 1097 mClearSearchOnPause = true; 1098 } 1099 1100 @Override 1101 public void onShortcutIntentCreated(Intent intent) { 1102 Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring."); 1103 } 1104 1105 @Override 1106 public void onHomeInActionBarSelected() { 1107 exitSearchUi(); 1108 } 1109 1110 @Override 1111 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 1112 position = mListsFragment.getRtlPosition(position); 1113 // Only scroll the button when the first tab is selected. The button should scroll from 1114 // the middle to right position only on the transition from the first tab to the second 1115 // tab. 1116 // If the app is in RTL mode, we need to check against the second tab, rather than the 1117 // first. This is because if we are scrolling between the first and second tabs, the 1118 // viewpager will report that the starting tab position is 1 rather than 0, due to the 1119 // reversal of the order of the tabs. 1120 final boolean isLayoutRtl = DialerUtils.isRtl(); 1121 final boolean shouldScrollButton = position == (isLayoutRtl 1122 ? ListsFragment.TAB_INDEX_RECENTS : ListsFragment.TAB_INDEX_SPEED_DIAL); 1123 if (shouldScrollButton && !mIsLandscape) { 1124 mFloatingActionButtonController.onPageScrolled( 1125 isLayoutRtl ? 1 - positionOffset : positionOffset); 1126 } else if (position != ListsFragment.TAB_INDEX_SPEED_DIAL) { 1127 mFloatingActionButtonController.onPageScrolled(1); 1128 } 1129 } 1130 1131 @Override 1132 public void onPageSelected(int position) { 1133 position = mListsFragment.getRtlPosition(position); 1134 mCurrentTabPosition = position; 1135 } 1136 1137 @Override 1138 public void onPageScrollStateChanged(int state) { 1139 } 1140 1141 private TelephonyManager getTelephonyManager() { 1142 return (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); 1143 } 1144 1145 private PhoneManager getPhoneManager() { 1146 return (PhoneManager) getSystemService(Context.PHONE_SERVICE); 1147 } 1148 1149 @Override 1150 public boolean isActionBarShowing() { 1151 return mActionBarController.isActionBarShowing(); 1152 } 1153 1154 public boolean isDialpadShown() { 1155 return mIsDialpadShown; 1156 } 1157 1158 @Override 1159 public int getActionBarHideOffset() { 1160 return getActionBar().getHideOffset(); 1161 } 1162 1163 @Override 1164 public int getActionBarHeight() { 1165 return mActionBarHeight; 1166 } 1167 1168 @Override 1169 public void setActionBarHideOffset(int hideOffset) { 1170 mActionBarController.setHideOffset(hideOffset); 1171 } 1172 1173 /** 1174 * Updates controller based on currently known information. 1175 * 1176 * @param animate Whether or not to animate the transition. 1177 */ 1178 private void updateFloatingActionButtonControllerAlignment(boolean animate) { 1179 int align; 1180 if (mIsDialpadShown) { 1181 align = mIsLandscape ? FloatingActionButtonController.ALIGN_QUARTER_END 1182 : FloatingActionButtonController.ALIGN_MIDDLE; 1183 } else { 1184 if (!mIsLandscape) { 1185 align = mCurrentTabPosition == ListsFragment.TAB_INDEX_SPEED_DIAL 1186 ? FloatingActionButtonController.ALIGN_MIDDLE 1187 : FloatingActionButtonController.ALIGN_END; 1188 } else { 1189 align = FloatingActionButtonController.ALIGN_END; 1190 } 1191 } 1192 mFloatingActionButtonController.align(align, 1193 0 /* offsetX */, 1194 mIsDialpadShown ? mFloatingActionButtonDialpadMarginBottomOffset : 0 /* offsetY */, 1195 animate); 1196 } 1197} 1198