DialtactsActivity.java revision 8204324a69c4d6b1b95351ab2863b5ddac6fb0a5
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 java.util.ArrayList; 95import java.util.List; 96 97/** 98 * The dialer tab's title is 'phone', a more common name (see strings.xml). 99 */ 100public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener, 101 DialpadFragment.OnDialpadQueryChangedListener, 102 OnListFragmentScrolledListener, 103 ListsFragment.HostInterface, 104 SpeedDialFragment.HostInterface, 105 SearchFragment.HostInterface, 106 OnDragDropListener, 107 OnPhoneNumberPickerActionListener, 108 PopupMenu.OnMenuItemClickListener, 109 ViewPager.OnPageChangeListener, 110 ActionBarController.ActivityUi { 111 private static final String TAG = "DialtactsActivity"; 112 113 public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 114 115 public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences"; 116 117 /** @see #getCallOrigin() */ 118 private static final String CALL_ORIGIN_DIALTACTS = 119 "com.android.dialer.DialtactsActivity"; 120 121 private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui"; 122 private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui"; 123 private static final String KEY_SEARCH_QUERY = "search_query"; 124 private static final String KEY_FIRST_LAUNCH = "first_launch"; 125 private static final String KEY_IS_DIALPAD_SHOWN = "is_dialpad_shown"; 126 127 private static final String TAG_DIALPAD_FRAGMENT = "dialpad"; 128 private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search"; 129 private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial"; 130 private static final String TAG_FAVORITES_FRAGMENT = "favorites"; 131 132 /** 133 * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}. 134 */ 135 private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER"; 136 137 private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1; 138 139 private FrameLayout mParentLayout; 140 141 /** 142 * Fragment containing the dialpad that slides into view 143 */ 144 protected DialpadFragment mDialpadFragment; 145 146 /** 147 * Fragment for searching phone numbers using the alphanumeric keyboard. 148 */ 149 private RegularSearchFragment mRegularSearchFragment; 150 151 /** 152 * Fragment for searching phone numbers using the dialpad. 153 */ 154 private SmartDialSearchFragment mSmartDialSearchFragment; 155 156 /** 157 * Animation that slides in. 158 */ 159 private Animation mSlideIn; 160 161 /** 162 * Animation that slides out. 163 */ 164 private Animation mSlideOut; 165 166 /** 167 * Listener for after slide out animation completes on dialer fragment. 168 */ 169 AnimationListenerAdapter mSlideOutListener = new AnimationListenerAdapter() { 170 @Override 171 public void onAnimationEnd(Animation animation) { 172 commitDialpadFragmentHide(); 173 } 174 }; 175 176 /** 177 * Fragment containing the speed dial list, recents list, and all contacts list. 178 */ 179 private ListsFragment mListsFragment; 180 181 /** 182 * Tracks whether onSaveInstanceState has been called. If true, no fragment transactions can 183 * be commited. 184 */ 185 private boolean mStateSaved; 186 private boolean mIsRestarting; 187 private boolean mInDialpadSearch; 188 private boolean mInRegularSearch; 189 private boolean mClearSearchOnPause; 190 private boolean mIsDialpadShown; 191 private boolean mShowDialpadOnResume; 192 193 /** 194 * Whether or not the device is in landscape orientation. 195 */ 196 private boolean mIsLandscape; 197 198 /** 199 * The position of the currently selected tab in the attached {@link ListsFragment}. 200 */ 201 private int mCurrentTabPosition = 0; 202 203 /** 204 * True if the dialpad is only temporarily showing due to being in call 205 */ 206 private boolean mInCallDialpadUp; 207 208 /** 209 * True when this activity has been launched for the first time. 210 */ 211 private boolean mFirstLaunch; 212 213 /** 214 * Search query to be applied to the SearchView in the ActionBar once 215 * onCreateOptionsMenu has been called. 216 */ 217 private String mPendingSearchViewQuery; 218 219 private PopupMenu mOverflowMenu; 220 private EditText mSearchView; 221 private View mVoiceSearchButton; 222 223 private String mSearchQuery; 224 225 private DialerDatabaseHelper mDialerDatabaseHelper; 226 private DragDropController mDragDropController; 227 private ActionBarController mActionBarController; 228 229 private FloatingActionButtonController mFloatingActionButtonController; 230 231 private int mActionBarHeight; 232 233 /** 234 * The text returned from a voice search query. Set in {@link #onActivityResult} and used in 235 * {@link #onResume()} to populate the search box. 236 */ 237 private String mVoiceSearchQuery; 238 239 private class OptionsPopupMenu extends PopupMenu { 240 public OptionsPopupMenu(Context context, View anchor) { 241 super(context, anchor, Gravity.END); 242 } 243 244 @Override 245 public void show() { 246 final Menu menu = getMenu(); 247 final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents); 248 clearFrequents.setVisible(mListsFragment != null && 249 mListsFragment.getSpeedDialFragment() != null && 250 mListsFragment.getSpeedDialFragment().hasFrequents()); 251 super.show(); 252 } 253 } 254 255 /** 256 * Listener that listens to drag events and sends their x and y coordinates to a 257 * {@link DragDropController}. 258 */ 259 private class LayoutOnDragListener implements OnDragListener { 260 @Override 261 public boolean onDrag(View v, DragEvent event) { 262 if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) { 263 mDragDropController.handleDragHovered(v, (int) event.getX(), (int) event.getY()); 264 } 265 return true; 266 } 267 } 268 269 /** 270 * Listener used to send search queries to the phone search fragment. 271 */ 272 private final TextWatcher mPhoneSearchQueryTextListener = new TextWatcher() { 273 @Override 274 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 275 } 276 277 @Override 278 public void onTextChanged(CharSequence s, int start, int before, int count) { 279 final String newText = s.toString(); 280 if (newText.equals(mSearchQuery)) { 281 // If the query hasn't changed (perhaps due to activity being destroyed 282 // and restored, or user launching the same DIAL intent twice), then there is 283 // no need to do anything here. 284 return; 285 } 286 if (DEBUG) { 287 Log.d(TAG, "onTextChange for mSearchView called with new query: " + newText); 288 Log.d(TAG, "Previous Query: " + mSearchQuery); 289 } 290 mSearchQuery = newText; 291 292 // Show search fragment only when the query string is changed to non-empty text. 293 if (!TextUtils.isEmpty(newText)) { 294 // Call enterSearchUi only if we are switching search modes, or showing a search 295 // fragment for the first time. 296 final boolean sameSearchMode = (mIsDialpadShown && mInDialpadSearch) || 297 (!mIsDialpadShown && mInRegularSearch); 298 if (!sameSearchMode) { 299 enterSearchUi(mIsDialpadShown, mSearchQuery); 300 } 301 } 302 303 if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { 304 mSmartDialSearchFragment.setQueryString(mSearchQuery, false /* delaySelection */); 305 } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { 306 mRegularSearchFragment.setQueryString(mSearchQuery, false /* delaySelection */); 307 } 308 } 309 310 @Override 311 public void afterTextChanged(Editable s) { 312 } 313 }; 314 315 316 /** 317 * Open the search UI when the user clicks on the search box. 318 */ 319 private final View.OnClickListener mSearchViewOnClickListener = new View.OnClickListener() { 320 @Override 321 public void onClick(View v) { 322 if (!isInSearchUi()) { 323 mActionBarController.onSearchBoxTapped(); 324 enterSearchUi(false /* smartDialSearch */, mSearchView.getText().toString()); 325 } 326 } 327 }; 328 329 /** 330 * If the search term is empty and the user closes the soft keyboard, close the search UI. 331 */ 332 private final View.OnKeyListener mSearchEditTextLayoutListener = new View.OnKeyListener() { 333 @Override 334 public boolean onKey(View v, int keyCode, KeyEvent event) { 335 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN && 336 TextUtils.isEmpty(mSearchView.getText().toString())) { 337 maybeExitSearchUi(); 338 } 339 return false; 340 } 341 }; 342 343 @Override 344 public boolean dispatchTouchEvent(MotionEvent ev) { 345 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 346 TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); 347 } 348 return super.dispatchTouchEvent(ev); 349 350 } 351 352 @Override 353 protected void onCreate(Bundle savedInstanceState) { 354 Trace.beginSection(TAG + " onCreate"); 355 super.onCreate(savedInstanceState); 356 mFirstLaunch = true; 357 358 final Resources resources = getResources(); 359 mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height_large); 360 361 Trace.beginSection(TAG + " setContentView"); 362 setContentView(R.layout.dialtacts_activity); 363 Trace.endSection(); 364 getWindow().setBackgroundDrawable(null); 365 366 Trace.beginSection(TAG + " setup Views"); 367 final ActionBar actionBar = getActionBar(); 368 actionBar.setCustomView(R.layout.search_edittext); 369 actionBar.setDisplayShowCustomEnabled(true); 370 actionBar.setBackgroundDrawable(null); 371 372 mActionBarController = new ActionBarController(this, 373 (SearchEditTextLayout) actionBar.getCustomView()); 374 375 SearchEditTextLayout searchEditTextLayout = 376 (SearchEditTextLayout) actionBar.getCustomView(); 377 searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener); 378 379 mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view); 380 mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); 381 mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button); 382 searchEditTextLayout.findViewById(R.id.search_magnifying_glass) 383 .setOnClickListener(mSearchViewOnClickListener); 384 searchEditTextLayout.findViewById(R.id.search_box_start_search) 385 .setOnClickListener(mSearchViewOnClickListener); 386 searchEditTextLayout.setOnBackButtonClickedListener(new OnBackButtonClickedListener() { 387 @Override 388 public void onBackButtonClicked() { 389 onBackPressed(); 390 } 391 }); 392 393 mIsLandscape = getResources().getConfiguration().orientation 394 == Configuration.ORIENTATION_LANDSCAPE; 395 396 final View floatingActionButtonContainer = findViewById( 397 R.id.floating_action_button_container); 398 ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button); 399 floatingActionButton.setOnClickListener(this); 400 mFloatingActionButtonController = new FloatingActionButtonController(this, 401 floatingActionButtonContainer, floatingActionButton); 402 403 ImageButton optionsMenuButton = 404 (ImageButton) searchEditTextLayout.findViewById(R.id.dialtacts_options_menu_button); 405 optionsMenuButton.setOnClickListener(this); 406 mOverflowMenu = buildOptionsMenu(searchEditTextLayout); 407 optionsMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener()); 408 409 // Add the favorites fragment, and the dialpad fragment, but only if savedInstanceState 410 // is null. Otherwise the fragment manager takes care of recreating these fragments. 411 if (savedInstanceState == null) { 412 getFragmentManager().beginTransaction() 413 .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT) 414 .add(R.id.dialtacts_container, new DialpadFragment(), TAG_DIALPAD_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 (!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 mDialpadFragment.setAnimate(animate); 671 mListsFragment.setUserVisibleHint(false); 672 AnalyticsUtil.sendScreenView(mDialpadFragment); 673 674 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 675 ft.show(mDialpadFragment); 676 ft.commit(); 677 678 if (animate) { 679 mFloatingActionButtonController.scaleOut(); 680 } else { 681 mFloatingActionButtonController.setVisible(false); 682 } 683 mActionBarController.onDialpadUp(); 684 685 if (!isInSearchUi()) { 686 enterSearchUi(true /* isSmartDial */, mSearchQuery); 687 } 688 } 689 690 /** 691 * Callback from child DialpadFragment when the dialpad is shown. 692 */ 693 public void onDialpadShown() { 694 if (mDialpadFragment.getAnimate()) { 695 mDialpadFragment.getView().startAnimation(mSlideIn); 696 } else { 697 mDialpadFragment.setYFraction(0); 698 } 699 700 updateSearchFragmentPosition(); 701 } 702 703 /** 704 * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in 705 * a callback after the hide animation ends. 706 * @see #commitDialpadFragmentHide 707 */ 708 public void hideDialpadFragment(boolean animate, boolean clearDialpad) { 709 if (mDialpadFragment == null) { 710 return; 711 } 712 if (clearDialpad) { 713 mDialpadFragment.clearDialpad(); 714 } 715 if (!mIsDialpadShown) { 716 return; 717 } 718 mIsDialpadShown = false; 719 mDialpadFragment.setAnimate(animate); 720 mListsFragment.setUserVisibleHint(true); 721 mListsFragment.sendScreenViewForCurrentPosition(); 722 723 updateSearchFragmentPosition(); 724 725 updateFloatingActionButtonControllerAlignment(animate); 726 if (animate) { 727 mDialpadFragment.getView().startAnimation(mSlideOut); 728 } else { 729 commitDialpadFragmentHide(); 730 } 731 732 mActionBarController.onDialpadDown(); 733 734 if (isInSearchUi()) { 735 if (TextUtils.isEmpty(mSearchQuery)) { 736 exitSearchUi(); 737 } 738 } 739 } 740 741 /** 742 * Finishes hiding the dialpad fragment after any animations are completed. 743 */ 744 private void commitDialpadFragmentHide() { 745 if (!mStateSaved && !mDialpadFragment.isHidden()) { 746 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 747 ft.hide(mDialpadFragment); 748 ft.commit(); 749 } 750 mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); 751 } 752 753 private void updateSearchFragmentPosition() { 754 SearchFragment fragment = null; 755 if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { 756 fragment = mSmartDialSearchFragment; 757 } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { 758 fragment = mRegularSearchFragment; 759 } 760 if (fragment != null && fragment.isVisible()) { 761 fragment.updatePosition(true /* animate */); 762 } 763 } 764 765 @Override 766 public boolean isInSearchUi() { 767 return mInDialpadSearch || mInRegularSearch; 768 } 769 770 @Override 771 public boolean hasSearchQuery() { 772 return !TextUtils.isEmpty(mSearchQuery); 773 } 774 775 @Override 776 public boolean shouldShowActionBar() { 777 return mListsFragment.shouldShowActionBar(); 778 } 779 780 private void setNotInSearchUi() { 781 mInDialpadSearch = false; 782 mInRegularSearch = false; 783 } 784 785 private void hideDialpadAndSearchUi() { 786 if (mIsDialpadShown) { 787 hideDialpadFragment(false, true); 788 } else { 789 exitSearchUi(); 790 } 791 } 792 793 private void prepareVoiceSearchButton() { 794 final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 795 if (canIntentBeHandled(voiceIntent)) { 796 mVoiceSearchButton.setVisibility(View.VISIBLE); 797 mVoiceSearchButton.setOnClickListener(this); 798 } else { 799 mVoiceSearchButton.setVisibility(View.GONE); 800 } 801 } 802 803 private OptionsPopupMenu buildOptionsMenu(View invoker) { 804 final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker); 805 popupMenu.inflate(R.menu.dialtacts_options); 806 final Menu menu = popupMenu.getMenu(); 807 popupMenu.setOnMenuItemClickListener(this); 808 return popupMenu; 809 } 810 811 @Override 812 public boolean onCreateOptionsMenu(Menu menu) { 813 if (mPendingSearchViewQuery != null) { 814 mSearchView.setText(mPendingSearchViewQuery); 815 mPendingSearchViewQuery = null; 816 } 817 mActionBarController.restoreActionBarOffset(); 818 return false; 819 } 820 821 /** 822 * Returns true if the intent is due to hitting the green send key (hardware call button: 823 * KEYCODE_CALL) while in a call. 824 * 825 * @param intent the intent that launched this activity 826 * @return true if the intent is due to hitting the green send key while in a call 827 */ 828 private boolean isSendKeyWhileInCall(Intent intent) { 829 // If there is a call in progress and the user launched the dialer by hitting the call 830 // button, go straight to the in-call screen. 831 final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction()); 832 833 if (callKey) { 834 getTelecomManager().showInCallScreen(false); 835 return true; 836 } 837 838 return false; 839 } 840 841 /** 842 * Sets the current tab based on the intent's request type 843 * 844 * @param intent Intent that contains information about which tab should be selected 845 */ 846 private void displayFragment(Intent intent) { 847 // If we got here by hitting send and we're in call forward along to the in-call activity 848 if (isSendKeyWhileInCall(intent)) { 849 finish(); 850 return; 851 } 852 853 if (mDialpadFragment != null) { 854 final boolean phoneIsInUse = phoneIsInUse(); 855 if (phoneIsInUse || (intent.getData() != null && isDialIntent(intent))) { 856 mDialpadFragment.setStartedFromNewIntent(true); 857 if (phoneIsInUse && !mDialpadFragment.isVisible()) { 858 mInCallDialpadUp = true; 859 } 860 showDialpadFragment(false); 861 } 862 } 863 } 864 865 @Override 866 public void onNewIntent(Intent newIntent) { 867 setIntent(newIntent); 868 mStateSaved = false; 869 displayFragment(newIntent); 870 871 invalidateOptionsMenu(); 872 } 873 874 /** Returns true if the given intent contains a phone number to populate the dialer with */ 875 private boolean isDialIntent(Intent intent) { 876 final String action = intent.getAction(); 877 if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) { 878 return true; 879 } 880 if (Intent.ACTION_VIEW.equals(action)) { 881 final Uri data = intent.getData(); 882 if (data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme())) { 883 return true; 884 } 885 } 886 return false; 887 } 888 889 /** 890 * Returns an appropriate call origin for this Activity. May return null when no call origin 891 * should be used (e.g. when some 3rd party application launched the screen. Call origin is 892 * for remembering the tab in which the user made a phone call, so the external app's DIAL 893 * request should not be counted.) 894 */ 895 public String getCallOrigin() { 896 return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null; 897 } 898 899 /** 900 * Shows the search fragment 901 */ 902 private void enterSearchUi(boolean smartDialSearch, String query) { 903 if (mStateSaved || getFragmentManager().isDestroyed()) { 904 // Weird race condition where fragment is doing work after the activity is destroyed 905 // due to talkback being on (b/10209937). Just return since we can't do any 906 // constructive here. 907 return; 908 } 909 910 if (DEBUG) { 911 Log.d(TAG, "Entering search UI - smart dial " + smartDialSearch); 912 } 913 914 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 915 if (mInDialpadSearch && mSmartDialSearchFragment != null) { 916 transaction.remove(mSmartDialSearchFragment); 917 } else if (mInRegularSearch && mRegularSearchFragment != null) { 918 transaction.remove(mRegularSearchFragment); 919 } 920 921 final String tag; 922 if (smartDialSearch) { 923 tag = TAG_SMARTDIAL_SEARCH_FRAGMENT; 924 } else { 925 tag = TAG_REGULAR_SEARCH_FRAGMENT; 926 } 927 mInDialpadSearch = smartDialSearch; 928 mInRegularSearch = !smartDialSearch; 929 930 SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag); 931 transaction.setCustomAnimations(android.R.animator.fade_in, 0); 932 if (fragment == null) { 933 if (smartDialSearch) { 934 fragment = new SmartDialSearchFragment(); 935 } else { 936 fragment = new RegularSearchFragment(); 937 } 938 transaction.add(R.id.dialtacts_frame, fragment, tag); 939 } else { 940 transaction.show(fragment); 941 } 942 // DialtactsActivity will provide the options menu 943 fragment.setHasOptionsMenu(false); 944 fragment.setShowEmptyListForNullQuery(true); 945 fragment.setQueryString(query, false /* delaySelection */); 946 transaction.commit(); 947 948 mListsFragment.getView().animate().alpha(0).withLayer(); 949 mListsFragment.setUserVisibleHint(false); 950 } 951 952 /** 953 * Hides the search fragment 954 */ 955 private void exitSearchUi() { 956 // See related bug in enterSearchUI(); 957 if (getFragmentManager().isDestroyed() || mStateSaved) { 958 return; 959 } 960 961 mSearchView.setText(null); 962 mDialpadFragment.clearDialpad(); 963 setNotInSearchUi(); 964 965 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 966 if (mSmartDialSearchFragment != null) { 967 transaction.remove(mSmartDialSearchFragment); 968 } 969 if (mRegularSearchFragment != null) { 970 transaction.remove(mRegularSearchFragment); 971 } 972 transaction.commit(); 973 974 mListsFragment.getView().animate().alpha(1).withLayer(); 975 if (!mDialpadFragment.isVisible()) { 976 // If the dialpad fragment wasn't previously visible, then send a screen view because 977 // we are exiting regular search. Otherwise, the screen view will be sent by 978 // {@link #hideDialpadFragment}. 979 mListsFragment.sendScreenViewForCurrentPosition(); 980 mListsFragment.setUserVisibleHint(true); 981 } 982 983 mActionBarController.onSearchUiExited(); 984 } 985 986 @Override 987 public void onBackPressed() { 988 if (mStateSaved) { 989 return; 990 } 991 if (mIsDialpadShown) { 992 if (TextUtils.isEmpty(mSearchQuery) || 993 (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible() 994 && mSmartDialSearchFragment.getAdapter().getCount() == 0)) { 995 exitSearchUi(); 996 } 997 hideDialpadFragment(true, false); 998 } else if (isInSearchUi()) { 999 exitSearchUi(); 1000 DialerUtils.hideInputMethod(mParentLayout); 1001 } else { 1002 super.onBackPressed(); 1003 } 1004 } 1005 1006 /** 1007 * @return True if the search UI was exited, false otherwise 1008 */ 1009 private boolean maybeExitSearchUi() { 1010 if (isInSearchUi() && TextUtils.isEmpty(mSearchQuery)) { 1011 exitSearchUi(); 1012 DialerUtils.hideInputMethod(mParentLayout); 1013 return true; 1014 } 1015 return false; 1016 } 1017 1018 @Override 1019 public void onDialpadQueryChanged(String query) { 1020 if (mSmartDialSearchFragment != null) { 1021 mSmartDialSearchFragment.setAddToContactNumber(query); 1022 } 1023 final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query, 1024 SmartDialNameMatcher.LATIN_SMART_DIAL_MAP); 1025 1026 if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) { 1027 if (DEBUG) { 1028 Log.d(TAG, "onDialpadQueryChanged - new query: " + query); 1029 } 1030 if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { 1031 // This callback can happen if the dialpad fragment is recreated because of 1032 // activity destruction. In that case, don't update the search view because 1033 // that would bring the user back to the search fragment regardless of the 1034 // previous state of the application. Instead, just return here and let the 1035 // fragment manager correctly figure out whatever fragment was last displayed. 1036 if (!TextUtils.isEmpty(normalizedQuery)) { 1037 mPendingSearchViewQuery = normalizedQuery; 1038 } 1039 return; 1040 } 1041 mSearchView.setText(normalizedQuery); 1042 } 1043 } 1044 1045 @Override 1046 public void onListFragmentScrollStateChange(int scrollState) { 1047 if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 1048 hideDialpadFragment(true, false); 1049 DialerUtils.hideInputMethod(mParentLayout); 1050 } 1051 } 1052 1053 @Override 1054 public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, 1055 int totalItemCount) { 1056 // TODO: No-op for now. This should eventually show/hide the actionBar based on 1057 // interactions with the ListsFragments. 1058 } 1059 1060 private boolean phoneIsInUse() { 1061 return getTelecomManager().isInCall(); 1062 } 1063 1064 public static Intent getAddNumberToContactIntent(CharSequence text) { 1065 return getAddToContactIntent(null /* name */, text /* phoneNumber */, 1066 -1 /* phoneNumberType */); 1067 } 1068 1069 public static Intent getAddToContactIntent(CharSequence name, CharSequence phoneNumber, 1070 int phoneNumberType) { 1071 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 1072 intent.putExtra(Intents.Insert.PHONE, phoneNumber); 1073 // Only include the name and phone type extras if they are specified (the method 1074 // getAddNumberToContactIntent does not use them). 1075 if (name != null) { 1076 intent.putExtra(Intents.Insert.NAME, name); 1077 } 1078 if (phoneNumberType != -1) { 1079 intent.putExtra(Intents.Insert.PHONE_TYPE, phoneNumberType); 1080 } 1081 intent.setType(Contacts.CONTENT_ITEM_TYPE); 1082 return intent; 1083 } 1084 1085 private boolean canIntentBeHandled(Intent intent) { 1086 final PackageManager packageManager = getPackageManager(); 1087 final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent, 1088 PackageManager.MATCH_DEFAULT_ONLY); 1089 return resolveInfo != null && resolveInfo.size() > 0; 1090 } 1091 1092 @Override 1093 public void showCallHistory() { 1094 // Use explicit CallLogActivity intent instead of ACTION_VIEW + 1095 // CONTENT_TYPE, so that we always open our call log from our dialer 1096 final Intent intent = new Intent(this, CallLogActivity.class); 1097 startActivity(intent); 1098 } 1099 1100 /** 1101 * Called when the user has long-pressed a contact tile to start a drag operation. 1102 */ 1103 @Override 1104 public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { 1105 if (mListsFragment.isPaneOpen()) { 1106 mActionBarController.setAlpha(ListsFragment.REMOVE_VIEW_SHOWN_ALPHA); 1107 } 1108 mListsFragment.showRemoveView(true); 1109 } 1110 1111 @Override 1112 public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) { 1113 } 1114 1115 /** 1116 * Called when the user has released a contact tile after long-pressing it. 1117 */ 1118 @Override 1119 public void onDragFinished(int x, int y) { 1120 if (mListsFragment.isPaneOpen()) { 1121 mActionBarController.setAlpha(ListsFragment.REMOVE_VIEW_HIDDEN_ALPHA); 1122 } 1123 mListsFragment.showRemoveView(false); 1124 } 1125 1126 @Override 1127 public void onDroppedOnRemove() {} 1128 1129 /** 1130 * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer 1131 * once it has been attached to the activity. 1132 */ 1133 @Override 1134 public void setDragDropController(DragDropController dragController) { 1135 mDragDropController = dragController; 1136 mListsFragment.getRemoveView().setDragDropController(dragController); 1137 } 1138 1139 @Override 1140 public void onPickPhoneNumberAction(Uri dataUri) { 1141 // Specify call-origin so that users will see the previous tab instead of 1142 // CallLog screen (search UI will be automatically exited). 1143 PhoneNumberInteraction.startInteractionForPhoneCall( 1144 DialtactsActivity.this, dataUri, getCallOrigin()); 1145 mClearSearchOnPause = true; 1146 } 1147 1148 @Override 1149 public void onCallNumberDirectly(String phoneNumber) { 1150 onCallNumberDirectly(phoneNumber, false /* isVideoCall */); 1151 } 1152 1153 @Override 1154 public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall) { 1155 Intent intent = isVideoCall ? 1156 PrivilegedCallUtil.getVideoCallIntent(phoneNumber, getCallOrigin()) : 1157 PrivilegedCallUtil.getCallIntent(phoneNumber, getCallOrigin()); 1158 DialerUtils.startActivityWithErrorToast(this, intent); 1159 mClearSearchOnPause = true; 1160 } 1161 1162 @Override 1163 public void onShortcutIntentCreated(Intent intent) { 1164 Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring."); 1165 } 1166 1167 @Override 1168 public void onHomeInActionBarSelected() { 1169 exitSearchUi(); 1170 } 1171 1172 @Override 1173 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 1174 position = mListsFragment.getRtlPosition(position); 1175 // Only scroll the button when the first tab is selected. The button should scroll from 1176 // the middle to right position only on the transition from the first tab to the second 1177 // tab. 1178 // If the app is in RTL mode, we need to check against the second tab, rather than the 1179 // first. This is because if we are scrolling between the first and second tabs, the 1180 // viewpager will report that the starting tab position is 1 rather than 0, due to the 1181 // reversal of the order of the tabs. 1182 final boolean isLayoutRtl = DialerUtils.isRtl(); 1183 final boolean shouldScrollButton = position == (isLayoutRtl 1184 ? ListsFragment.TAB_INDEX_RECENTS : ListsFragment.TAB_INDEX_SPEED_DIAL); 1185 if (shouldScrollButton && !mIsLandscape) { 1186 mFloatingActionButtonController.onPageScrolled( 1187 isLayoutRtl ? 1 - positionOffset : positionOffset); 1188 } else if (position != ListsFragment.TAB_INDEX_SPEED_DIAL) { 1189 mFloatingActionButtonController.onPageScrolled(1); 1190 } 1191 } 1192 1193 @Override 1194 public void onPageSelected(int position) { 1195 position = mListsFragment.getRtlPosition(position); 1196 mCurrentTabPosition = position; 1197 } 1198 1199 @Override 1200 public void onPageScrollStateChanged(int state) { 1201 } 1202 1203 private TelephonyManager getTelephonyManager() { 1204 return (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); 1205 } 1206 1207 private TelecomManager getTelecomManager() { 1208 return (TelecomManager) getSystemService(Context.TELECOM_SERVICE); 1209 } 1210 1211 @Override 1212 public boolean isActionBarShowing() { 1213 return mActionBarController.isActionBarShowing(); 1214 } 1215 1216 @Override 1217 public ActionBarController getActionBarController() { 1218 return mActionBarController; 1219 } 1220 1221 public boolean isDialpadShown() { 1222 return mIsDialpadShown; 1223 } 1224 1225 @Override 1226 public int getActionBarHideOffset() { 1227 return getActionBar().getHideOffset(); 1228 } 1229 1230 @Override 1231 public void setActionBarHideOffset(int offset) { 1232 getActionBar().setHideOffset(offset); 1233 } 1234 1235 @Override 1236 public int getActionBarHeight() { 1237 return mActionBarHeight; 1238 } 1239 1240 /** 1241 * Updates controller based on currently known information. 1242 * 1243 * @param animate Whether or not to animate the transition. 1244 */ 1245 private void updateFloatingActionButtonControllerAlignment(boolean animate) { 1246 int align = (!mIsLandscape && mCurrentTabPosition == ListsFragment.TAB_INDEX_SPEED_DIAL) ? 1247 FloatingActionButtonController.ALIGN_MIDDLE : 1248 FloatingActionButtonController.ALIGN_END; 1249 mFloatingActionButtonController.align(align, 0 /* offsetX */, 0 /* offsetY */, animate); 1250 } 1251} 1252