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