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