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