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