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