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