DialtactsActivity.java revision 8a397adffe40af39fe4ca20865cc3748a29376a1
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 mActionBarController = new ActionBarController(this, 375 (SearchEditTextLayout) actionBar.getCustomView()); 376 377 SearchEditTextLayout searchEditTextLayout = 378 (SearchEditTextLayout) actionBar.getCustomView(); 379 searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener); 380 381 mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view); 382 mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); 383 mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button); 384 searchEditTextLayout.findViewById(R.id.search_magnifying_glass) 385 .setOnClickListener(mSearchViewOnClickListener); 386 searchEditTextLayout.findViewById(R.id.search_box_start_search) 387 .setOnClickListener(mSearchViewOnClickListener); 388 searchEditTextLayout.setOnBackButtonClickedListener(new OnBackButtonClickedListener() { 389 @Override 390 public void onBackButtonClicked() { 391 onBackPressed(); 392 } 393 }); 394 395 mIsLandscape = getResources().getConfiguration().orientation 396 == Configuration.ORIENTATION_LANDSCAPE; 397 398 final View floatingActionButtonContainer = findViewById( 399 R.id.floating_action_button_container); 400 ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button); 401 floatingActionButton.setOnClickListener(this); 402 mFloatingActionButtonController = new FloatingActionButtonController(this, 403 floatingActionButtonContainer, floatingActionButton); 404 405 ImageButton optionsMenuButton = 406 (ImageButton) searchEditTextLayout.findViewById(R.id.dialtacts_options_menu_button); 407 optionsMenuButton.setOnClickListener(this); 408 mOverflowMenu = buildOptionsMenu(searchEditTextLayout); 409 optionsMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener()); 410 411 // Add the favorites fragment but only if savedInstanceState is null. Otherwise the 412 // fragment manager is responsible for recreating it. 413 if (savedInstanceState == null) { 414 getFragmentManager().beginTransaction() 415 .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT) 416 .commit(); 417 } else { 418 mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY); 419 mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI); 420 mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI); 421 mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH); 422 mShowDialpadOnResume = savedInstanceState.getBoolean(KEY_IS_DIALPAD_SHOWN); 423 mActionBarController.restoreInstanceState(savedInstanceState); 424 } 425 426 final boolean isLayoutRtl = DialerUtils.isRtl(); 427 if (mIsLandscape) { 428 mSlideIn = AnimationUtils.loadAnimation(this, 429 isLayoutRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right); 430 mSlideOut = AnimationUtils.loadAnimation(this, 431 isLayoutRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right); 432 } else { 433 mSlideIn = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom); 434 mSlideOut = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom); 435 } 436 437 mSlideIn.setInterpolator(AnimUtils.EASE_IN); 438 mSlideOut.setInterpolator(AnimUtils.EASE_OUT); 439 440 mSlideOut.setAnimationListener(mSlideOutListener); 441 442 mParentLayout = (FrameLayout) findViewById(R.id.dialtacts_mainlayout); 443 mParentLayout.setOnDragListener(new LayoutOnDragListener()); 444 floatingActionButtonContainer.getViewTreeObserver().addOnGlobalLayoutListener( 445 new ViewTreeObserver.OnGlobalLayoutListener() { 446 @Override 447 public void onGlobalLayout() { 448 final ViewTreeObserver observer = 449 floatingActionButtonContainer.getViewTreeObserver(); 450 if (!observer.isAlive()) { 451 return; 452 } 453 observer.removeOnGlobalLayoutListener(this); 454 int screenWidth = mParentLayout.getWidth(); 455 mFloatingActionButtonController.setScreenWidth(screenWidth); 456 updateFloatingActionButtonControllerAlignment(false /* animate */); 457 } 458 }); 459 460 setupActivityOverlay(); 461 462 Trace.endSection(); 463 464 Trace.beginSection(TAG + " initialize smart dialing"); 465 mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this); 466 SmartDialPrefix.initializeNanpSettings(this); 467 Trace.endSection(); 468 Trace.endSection(); 469 } 470 471 private void setupActivityOverlay() { 472 final View activityOverlay = findViewById(R.id.activity_overlay); 473 activityOverlay.setOnTouchListener(new OnTouchListener() { 474 @Override 475 public boolean onTouch(View v, MotionEvent event) { 476 if (!mIsDialpadShown) { 477 maybeExitSearchUi(); 478 } 479 return false; 480 } 481 }); 482 } 483 484 @Override 485 protected void onResume() { 486 Trace.beginSection(TAG + " onResume"); 487 super.onResume(); 488 mStateSaved = false; 489 if (mFirstLaunch) { 490 displayFragment(getIntent()); 491 } else if (!phoneIsInUse() && mInCallDialpadUp) { 492 hideDialpadFragment(false, true); 493 mInCallDialpadUp = false; 494 } else if (mShowDialpadOnResume) { 495 showDialpadFragment(false); 496 mShowDialpadOnResume = false; 497 } 498 499 // If there was a voice query result returned in the {@link #onActivityResult} callback, it 500 // will have been stashed in mVoiceSearchQuery since the search results fragment cannot be 501 // shown until onResume has completed. Active the search UI and set the search term now. 502 if (!TextUtils.isEmpty(mVoiceSearchQuery)) { 503 mActionBarController.onSearchBoxTapped(); 504 mSearchView.setText(mVoiceSearchQuery); 505 mVoiceSearchQuery = null; 506 } 507 508 mFirstLaunch = false; 509 510 if (mIsRestarting) { 511 // This is only called when the activity goes from resumed -> paused -> resumed, so it 512 // will not cause an extra view to be sent out on rotation 513 if (mIsDialpadShown) { 514 AnalyticsUtil.sendScreenView(mDialpadFragment, this); 515 } 516 mIsRestarting = false; 517 } 518 prepareVoiceSearchButton(); 519 mDialerDatabaseHelper.startSmartDialUpdateThread(); 520 updateFloatingActionButtonControllerAlignment(false /* animate */); 521 Trace.endSection(); 522 } 523 524 @Override 525 protected void onRestart() { 526 super.onRestart(); 527 mIsRestarting = true; 528 } 529 530 @Override 531 protected void onPause() { 532 if (mClearSearchOnPause) { 533 hideDialpadAndSearchUi(); 534 mClearSearchOnPause = false; 535 } 536 if (mSlideOut.hasStarted() && !mSlideOut.hasEnded()) { 537 commitDialpadFragmentHide(); 538 } 539 super.onPause(); 540 } 541 542 @Override 543 protected void onSaveInstanceState(Bundle outState) { 544 super.onSaveInstanceState(outState); 545 outState.putString(KEY_SEARCH_QUERY, mSearchQuery); 546 outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch); 547 outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch); 548 outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch); 549 outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown); 550 mActionBarController.saveInstanceState(outState); 551 mStateSaved = true; 552 } 553 554 @Override 555 public void onAttachFragment(Fragment fragment) { 556 if (fragment instanceof DialpadFragment) { 557 mDialpadFragment = (DialpadFragment) fragment; 558 if (!mIsDialpadShown && !mShowDialpadOnResume) { 559 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 560 transaction.hide(mDialpadFragment); 561 transaction.commit(); 562 } 563 } else if (fragment instanceof SmartDialSearchFragment) { 564 mSmartDialSearchFragment = (SmartDialSearchFragment) fragment; 565 mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this); 566 } else if (fragment instanceof SearchFragment) { 567 mRegularSearchFragment = (RegularSearchFragment) fragment; 568 mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this); 569 } else if (fragment instanceof ListsFragment) { 570 mListsFragment = (ListsFragment) fragment; 571 mListsFragment.addOnPageChangeListener(this); 572 } 573 } 574 575 protected void handleMenuSettings() { 576 final Intent intent = new Intent(this, DialerSettingsActivity.class); 577 startActivity(intent); 578 } 579 580 @Override 581 public void onClick(View view) { 582 switch (view.getId()) { 583 case R.id.floating_action_button: 584 if (!mIsDialpadShown) { 585 mInCallDialpadUp = false; 586 showDialpadFragment(true); 587 } 588 break; 589 case R.id.voice_search_button: 590 try { 591 startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 592 ACTIVITY_REQUEST_CODE_VOICE_SEARCH); 593 } catch (ActivityNotFoundException e) { 594 Toast.makeText(DialtactsActivity.this, R.string.voice_search_not_available, 595 Toast.LENGTH_SHORT).show(); 596 } 597 break; 598 case R.id.dialtacts_options_menu_button: 599 mOverflowMenu.show(); 600 break; 601 default: { 602 Log.wtf(TAG, "Unexpected onClick event from " + view); 603 break; 604 } 605 } 606 } 607 608 @Override 609 public boolean onMenuItemClick(MenuItem item) { 610 switch (item.getItemId()) { 611 case R.id.menu_history: 612 showCallHistory(); 613 break; 614 case R.id.menu_add_contact: 615 try { 616 startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI)); 617 } catch (ActivityNotFoundException e) { 618 Toast toast = Toast.makeText(this, 619 R.string.add_contact_not_available, 620 Toast.LENGTH_SHORT); 621 toast.show(); 622 } 623 break; 624 case R.id.menu_import_export: 625 // We hard-code the "contactsAreAvailable" argument because doing it properly would 626 // involve querying a {@link ProviderStatusLoader}, which we don't want to do right 627 // now in Dialtacts for (potential) performance reasons. Compare with how it is 628 // done in {@link PeopleActivity}. 629 ImportExportDialogFragment.show(getFragmentManager(), true, 630 DialtactsActivity.class); 631 return true; 632 case R.id.menu_clear_frequents: 633 ClearFrequentsDialog.show(getFragmentManager()); 634 return true; 635 case R.id.menu_call_settings: 636 handleMenuSettings(); 637 return true; 638 } 639 return false; 640 } 641 642 @Override 643 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 644 if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) { 645 if (resultCode == RESULT_OK) { 646 final ArrayList<String> matches = data.getStringArrayListExtra( 647 RecognizerIntent.EXTRA_RESULTS); 648 if (matches.size() > 0) { 649 final String match = matches.get(0); 650 mVoiceSearchQuery = match; 651 } else { 652 Log.e(TAG, "Voice search - nothing heard"); 653 } 654 } else { 655 Log.e(TAG, "Voice search failed"); 656 } 657 } 658 super.onActivityResult(requestCode, resultCode, data); 659 } 660 661 /** 662 * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual 663 * updates are handled by a callback which is invoked after the dialpad fragment is shown. 664 * @see #onDialpadShown 665 */ 666 private void showDialpadFragment(boolean animate) { 667 if (mIsDialpadShown || mStateSaved) { 668 return; 669 } 670 mIsDialpadShown = true; 671 672 mListsFragment.setUserVisibleHint(false); 673 674 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 675 if (mDialpadFragment == null) { 676 mDialpadFragment = new DialpadFragment(); 677 ft.add(R.id.dialtacts_container, mDialpadFragment, TAG_DIALPAD_FRAGMENT); 678 } else { 679 ft.show(mDialpadFragment); 680 } 681 682 mDialpadFragment.setAnimate(animate); 683 AnalyticsUtil.sendScreenView(mDialpadFragment); 684 ft.commit(); 685 686 if (animate) { 687 mFloatingActionButtonController.scaleOut(); 688 } else { 689 mFloatingActionButtonController.setVisible(false); 690 } 691 mActionBarController.onDialpadUp(); 692 693 if (!isInSearchUi()) { 694 enterSearchUi(true /* isSmartDial */, mSearchQuery); 695 } 696 } 697 698 /** 699 * Callback from child DialpadFragment when the dialpad is shown. 700 */ 701 public void onDialpadShown() { 702 Assert.assertNotNull(mDialpadFragment); 703 if (mDialpadFragment.getAnimate()) { 704 mDialpadFragment.getView().startAnimation(mSlideIn); 705 } else { 706 mDialpadFragment.setYFraction(0); 707 } 708 709 updateSearchFragmentPosition(); 710 } 711 712 /** 713 * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in 714 * a callback after the hide animation ends. 715 * @see #commitDialpadFragmentHide 716 */ 717 public void hideDialpadFragment(boolean animate, boolean clearDialpad) { 718 if (mDialpadFragment == null) { 719 return; 720 } 721 if (clearDialpad) { 722 mDialpadFragment.clearDialpad(); 723 } 724 if (!mIsDialpadShown) { 725 return; 726 } 727 mIsDialpadShown = false; 728 mDialpadFragment.setAnimate(animate); 729 mListsFragment.setUserVisibleHint(true); 730 mListsFragment.sendScreenViewForCurrentPosition(); 731 732 updateSearchFragmentPosition(); 733 734 updateFloatingActionButtonControllerAlignment(animate); 735 if (animate) { 736 mDialpadFragment.getView().startAnimation(mSlideOut); 737 } else { 738 commitDialpadFragmentHide(); 739 } 740 741 mActionBarController.onDialpadDown(); 742 743 if (isInSearchUi()) { 744 if (TextUtils.isEmpty(mSearchQuery)) { 745 exitSearchUi(); 746 } 747 } 748 } 749 750 /** 751 * Finishes hiding the dialpad fragment after any animations are completed. 752 */ 753 private void commitDialpadFragmentHide() { 754 if (!mStateSaved && mDialpadFragment != null && !mDialpadFragment.isHidden()) { 755 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 756 ft.hide(mDialpadFragment); 757 ft.commit(); 758 } 759 mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); 760 } 761 762 private void updateSearchFragmentPosition() { 763 SearchFragment fragment = null; 764 if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { 765 fragment = mSmartDialSearchFragment; 766 } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { 767 fragment = mRegularSearchFragment; 768 } 769 if (fragment != null && fragment.isVisible()) { 770 fragment.updatePosition(true /* animate */); 771 } 772 } 773 774 @Override 775 public boolean isInSearchUi() { 776 return mInDialpadSearch || mInRegularSearch; 777 } 778 779 @Override 780 public boolean hasSearchQuery() { 781 return !TextUtils.isEmpty(mSearchQuery); 782 } 783 784 @Override 785 public boolean shouldShowActionBar() { 786 return mListsFragment.shouldShowActionBar(); 787 } 788 789 private void setNotInSearchUi() { 790 mInDialpadSearch = false; 791 mInRegularSearch = false; 792 } 793 794 private void hideDialpadAndSearchUi() { 795 if (mIsDialpadShown) { 796 hideDialpadFragment(false, true); 797 } else { 798 exitSearchUi(); 799 } 800 } 801 802 private void prepareVoiceSearchButton() { 803 final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 804 if (canIntentBeHandled(voiceIntent)) { 805 mVoiceSearchButton.setVisibility(View.VISIBLE); 806 mVoiceSearchButton.setOnClickListener(this); 807 } else { 808 mVoiceSearchButton.setVisibility(View.GONE); 809 } 810 } 811 812 private OptionsPopupMenu buildOptionsMenu(View invoker) { 813 final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker); 814 popupMenu.inflate(R.menu.dialtacts_options); 815 final Menu menu = popupMenu.getMenu(); 816 popupMenu.setOnMenuItemClickListener(this); 817 return popupMenu; 818 } 819 820 @Override 821 public boolean onCreateOptionsMenu(Menu menu) { 822 if (mPendingSearchViewQuery != null) { 823 mSearchView.setText(mPendingSearchViewQuery); 824 mPendingSearchViewQuery = null; 825 } 826 mActionBarController.restoreActionBarOffset(); 827 return false; 828 } 829 830 /** 831 * Returns true if the intent is due to hitting the green send key (hardware call button: 832 * KEYCODE_CALL) while in a call. 833 * 834 * @param intent the intent that launched this activity 835 * @return true if the intent is due to hitting the green send key while in a call 836 */ 837 private boolean isSendKeyWhileInCall(Intent intent) { 838 // If there is a call in progress and the user launched the dialer by hitting the call 839 // button, go straight to the in-call screen. 840 final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction()); 841 842 if (callKey) { 843 getTelecomManager().showInCallScreen(false); 844 return true; 845 } 846 847 return false; 848 } 849 850 /** 851 * Sets the current tab based on the intent's request type 852 * 853 * @param intent Intent that contains information about which tab should be selected 854 */ 855 private void displayFragment(Intent intent) { 856 // If we got here by hitting send and we're in call forward along to the in-call activity 857 if (isSendKeyWhileInCall(intent)) { 858 finish(); 859 return; 860 } 861 862 final boolean phoneIsInUse = phoneIsInUse(); 863 if (phoneIsInUse || (intent.getData() != null && isDialIntent(intent))) { 864 showDialpadFragment(false); 865 mDialpadFragment.setStartedFromNewIntent(true); 866 if (phoneIsInUse && !mDialpadFragment.isVisible()) { 867 mInCallDialpadUp = true; 868 } 869 } 870 } 871 872 @Override 873 public void onNewIntent(Intent newIntent) { 874 setIntent(newIntent); 875 mStateSaved = false; 876 displayFragment(newIntent); 877 878 invalidateOptionsMenu(); 879 } 880 881 /** Returns true if the given intent contains a phone number to populate the dialer with */ 882 private boolean isDialIntent(Intent intent) { 883 final String action = intent.getAction(); 884 if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) { 885 return true; 886 } 887 if (Intent.ACTION_VIEW.equals(action)) { 888 final Uri data = intent.getData(); 889 if (data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme())) { 890 return true; 891 } 892 } 893 return false; 894 } 895 896 /** 897 * Returns an appropriate call origin for this Activity. May return null when no call origin 898 * should be used (e.g. when some 3rd party application launched the screen. Call origin is 899 * for remembering the tab in which the user made a phone call, so the external app's DIAL 900 * request should not be counted.) 901 */ 902 public String getCallOrigin() { 903 return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null; 904 } 905 906 /** 907 * Shows the search fragment 908 */ 909 private void enterSearchUi(boolean smartDialSearch, String query) { 910 if (mStateSaved || getFragmentManager().isDestroyed()) { 911 // Weird race condition where fragment is doing work after the activity is destroyed 912 // due to talkback being on (b/10209937). Just return since we can't do any 913 // constructive here. 914 return; 915 } 916 917 if (DEBUG) { 918 Log.d(TAG, "Entering search UI - smart dial " + smartDialSearch); 919 } 920 921 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 922 if (mInDialpadSearch && mSmartDialSearchFragment != null) { 923 transaction.remove(mSmartDialSearchFragment); 924 } else if (mInRegularSearch && mRegularSearchFragment != null) { 925 transaction.remove(mRegularSearchFragment); 926 } 927 928 final String tag; 929 if (smartDialSearch) { 930 tag = TAG_SMARTDIAL_SEARCH_FRAGMENT; 931 } else { 932 tag = TAG_REGULAR_SEARCH_FRAGMENT; 933 } 934 mInDialpadSearch = smartDialSearch; 935 mInRegularSearch = !smartDialSearch; 936 937 SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag); 938 transaction.setCustomAnimations(android.R.animator.fade_in, 0); 939 if (fragment == null) { 940 if (smartDialSearch) { 941 fragment = new SmartDialSearchFragment(); 942 } else { 943 fragment = new RegularSearchFragment(); 944 } 945 transaction.add(R.id.dialtacts_frame, fragment, tag); 946 } else { 947 transaction.show(fragment); 948 } 949 // DialtactsActivity will provide the options menu 950 fragment.setHasOptionsMenu(false); 951 fragment.setShowEmptyListForNullQuery(true); 952 fragment.setQueryString(query, false /* delaySelection */); 953 transaction.commit(); 954 955 mListsFragment.getView().animate().alpha(0).withLayer(); 956 mListsFragment.setUserVisibleHint(false); 957 } 958 959 /** 960 * Hides the search fragment 961 */ 962 private void exitSearchUi() { 963 // See related bug in enterSearchUI(); 964 if (getFragmentManager().isDestroyed() || mStateSaved) { 965 return; 966 } 967 968 mSearchView.setText(null); 969 970 if (mDialpadFragment != null) { 971 mDialpadFragment.clearDialpad(); 972 } 973 974 setNotInSearchUi(); 975 976 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 977 if (mSmartDialSearchFragment != null) { 978 transaction.remove(mSmartDialSearchFragment); 979 } 980 if (mRegularSearchFragment != null) { 981 transaction.remove(mRegularSearchFragment); 982 } 983 transaction.commit(); 984 985 mListsFragment.getView().animate().alpha(1).withLayer(); 986 987 if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { 988 // If the dialpad fragment wasn't previously visible, then send a screen view because 989 // we are exiting regular search. Otherwise, the screen view will be sent by 990 // {@link #hideDialpadFragment}. 991 mListsFragment.sendScreenViewForCurrentPosition(); 992 mListsFragment.setUserVisibleHint(true); 993 } 994 995 mActionBarController.onSearchUiExited(); 996 } 997 998 @Override 999 public void onBackPressed() { 1000 if (mStateSaved) { 1001 return; 1002 } 1003 if (mIsDialpadShown) { 1004 if (TextUtils.isEmpty(mSearchQuery) || 1005 (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible() 1006 && mSmartDialSearchFragment.getAdapter().getCount() == 0)) { 1007 exitSearchUi(); 1008 } 1009 hideDialpadFragment(true, false); 1010 } else if (isInSearchUi()) { 1011 exitSearchUi(); 1012 DialerUtils.hideInputMethod(mParentLayout); 1013 } else { 1014 super.onBackPressed(); 1015 } 1016 } 1017 1018 /** 1019 * @return True if the search UI was exited, false otherwise 1020 */ 1021 private boolean maybeExitSearchUi() { 1022 if (isInSearchUi() && TextUtils.isEmpty(mSearchQuery)) { 1023 exitSearchUi(); 1024 DialerUtils.hideInputMethod(mParentLayout); 1025 return true; 1026 } 1027 return false; 1028 } 1029 1030 @Override 1031 public void onDialpadQueryChanged(String query) { 1032 if (mSmartDialSearchFragment != null) { 1033 mSmartDialSearchFragment.setAddToContactNumber(query); 1034 } 1035 final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query, 1036 SmartDialNameMatcher.LATIN_SMART_DIAL_MAP); 1037 1038 if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) { 1039 if (DEBUG) { 1040 Log.d(TAG, "onDialpadQueryChanged - new query: " + query); 1041 } 1042 if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { 1043 // This callback can happen if the dialpad fragment is recreated because of 1044 // activity destruction. In that case, don't update the search view because 1045 // that would bring the user back to the search fragment regardless of the 1046 // previous state of the application. Instead, just return here and let the 1047 // fragment manager correctly figure out whatever fragment was last displayed. 1048 if (!TextUtils.isEmpty(normalizedQuery)) { 1049 mPendingSearchViewQuery = normalizedQuery; 1050 } 1051 return; 1052 } 1053 mSearchView.setText(normalizedQuery); 1054 } 1055 } 1056 1057 @Override 1058 public void onListFragmentScrollStateChange(int scrollState) { 1059 if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 1060 hideDialpadFragment(true, false); 1061 DialerUtils.hideInputMethod(mParentLayout); 1062 } 1063 } 1064 1065 @Override 1066 public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, 1067 int totalItemCount) { 1068 // TODO: No-op for now. This should eventually show/hide the actionBar based on 1069 // interactions with the ListsFragments. 1070 } 1071 1072 private boolean phoneIsInUse() { 1073 return getTelecomManager().isInCall(); 1074 } 1075 1076 public static Intent getAddNumberToContactIntent(CharSequence text) { 1077 return getAddToContactIntent(null /* name */, text /* phoneNumber */, 1078 -1 /* phoneNumberType */); 1079 } 1080 1081 public static Intent getAddToContactIntent(CharSequence name, CharSequence phoneNumber, 1082 int phoneNumberType) { 1083 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 1084 intent.putExtra(Intents.Insert.PHONE, phoneNumber); 1085 // Only include the name and phone type extras if they are specified (the method 1086 // getAddNumberToContactIntent does not use them). 1087 if (name != null) { 1088 intent.putExtra(Intents.Insert.NAME, name); 1089 } 1090 if (phoneNumberType != -1) { 1091 intent.putExtra(Intents.Insert.PHONE_TYPE, phoneNumberType); 1092 } 1093 intent.setType(Contacts.CONTENT_ITEM_TYPE); 1094 return intent; 1095 } 1096 1097 private boolean canIntentBeHandled(Intent intent) { 1098 final PackageManager packageManager = getPackageManager(); 1099 final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent, 1100 PackageManager.MATCH_DEFAULT_ONLY); 1101 return resolveInfo != null && resolveInfo.size() > 0; 1102 } 1103 1104 @Override 1105 public void showCallHistory() { 1106 // Use explicit CallLogActivity intent instead of ACTION_VIEW + 1107 // CONTENT_TYPE, so that we always open our call log from our dialer 1108 final Intent intent = new Intent(this, CallLogActivity.class); 1109 startActivity(intent); 1110 } 1111 1112 /** 1113 * Called when the user has long-pressed a contact tile to start a drag operation. 1114 */ 1115 @Override 1116 public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { 1117 if (mListsFragment.isPaneOpen()) { 1118 mActionBarController.setAlpha(ListsFragment.REMOVE_VIEW_SHOWN_ALPHA); 1119 } 1120 mListsFragment.showRemoveView(true); 1121 } 1122 1123 @Override 1124 public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) { 1125 } 1126 1127 /** 1128 * Called when the user has released a contact tile after long-pressing it. 1129 */ 1130 @Override 1131 public void onDragFinished(int x, int y) { 1132 if (mListsFragment.isPaneOpen()) { 1133 mActionBarController.setAlpha(ListsFragment.REMOVE_VIEW_HIDDEN_ALPHA); 1134 } 1135 mListsFragment.showRemoveView(false); 1136 } 1137 1138 @Override 1139 public void onDroppedOnRemove() {} 1140 1141 /** 1142 * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer 1143 * once it has been attached to the activity. 1144 */ 1145 @Override 1146 public void setDragDropController(DragDropController dragController) { 1147 mDragDropController = dragController; 1148 mListsFragment.getRemoveView().setDragDropController(dragController); 1149 } 1150 1151 @Override 1152 public void onPickPhoneNumberAction(Uri dataUri) { 1153 // Specify call-origin so that users will see the previous tab instead of 1154 // CallLog screen (search UI will be automatically exited). 1155 PhoneNumberInteraction.startInteractionForPhoneCall( 1156 DialtactsActivity.this, dataUri, getCallOrigin()); 1157 mClearSearchOnPause = true; 1158 } 1159 1160 @Override 1161 public void onCallNumberDirectly(String phoneNumber) { 1162 onCallNumberDirectly(phoneNumber, false /* isVideoCall */); 1163 } 1164 1165 @Override 1166 public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall) { 1167 Intent intent = isVideoCall ? 1168 PrivilegedCallUtil.getVideoCallIntent(phoneNumber, getCallOrigin()) : 1169 PrivilegedCallUtil.getCallIntent(phoneNumber, getCallOrigin()); 1170 DialerUtils.startActivityWithErrorToast(this, intent); 1171 mClearSearchOnPause = true; 1172 } 1173 1174 @Override 1175 public void onShortcutIntentCreated(Intent intent) { 1176 Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring."); 1177 } 1178 1179 @Override 1180 public void onHomeInActionBarSelected() { 1181 exitSearchUi(); 1182 } 1183 1184 @Override 1185 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 1186 position = mListsFragment.getRtlPosition(position); 1187 // Only scroll the button when the first tab is selected. The button should scroll from 1188 // the middle to right position only on the transition from the first tab to the second 1189 // tab. 1190 // If the app is in RTL mode, we need to check against the second tab, rather than the 1191 // first. This is because if we are scrolling between the first and second tabs, the 1192 // viewpager will report that the starting tab position is 1 rather than 0, due to the 1193 // reversal of the order of the tabs. 1194 final boolean isLayoutRtl = DialerUtils.isRtl(); 1195 final boolean shouldScrollButton = position == (isLayoutRtl 1196 ? ListsFragment.TAB_INDEX_RECENTS : ListsFragment.TAB_INDEX_SPEED_DIAL); 1197 if (shouldScrollButton && !mIsLandscape) { 1198 mFloatingActionButtonController.onPageScrolled( 1199 isLayoutRtl ? 1 - positionOffset : positionOffset); 1200 } else if (position != ListsFragment.TAB_INDEX_SPEED_DIAL) { 1201 mFloatingActionButtonController.onPageScrolled(1); 1202 } 1203 } 1204 1205 @Override 1206 public void onPageSelected(int position) { 1207 position = mListsFragment.getRtlPosition(position); 1208 mCurrentTabPosition = position; 1209 } 1210 1211 @Override 1212 public void onPageScrollStateChanged(int state) { 1213 } 1214 1215 private TelephonyManager getTelephonyManager() { 1216 return (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); 1217 } 1218 1219 private TelecomManager getTelecomManager() { 1220 return (TelecomManager) getSystemService(Context.TELECOM_SERVICE); 1221 } 1222 1223 @Override 1224 public boolean isActionBarShowing() { 1225 return mActionBarController.isActionBarShowing(); 1226 } 1227 1228 @Override 1229 public ActionBarController getActionBarController() { 1230 return mActionBarController; 1231 } 1232 1233 public boolean isDialpadShown() { 1234 return mIsDialpadShown; 1235 } 1236 1237 @Override 1238 public int getActionBarHideOffset() { 1239 return getActionBar().getHideOffset(); 1240 } 1241 1242 @Override 1243 public void setActionBarHideOffset(int offset) { 1244 getActionBar().setHideOffset(offset); 1245 } 1246 1247 @Override 1248 public int getActionBarHeight() { 1249 return mActionBarHeight; 1250 } 1251 1252 /** 1253 * Updates controller based on currently known information. 1254 * 1255 * @param animate Whether or not to animate the transition. 1256 */ 1257 private void updateFloatingActionButtonControllerAlignment(boolean animate) { 1258 int align = (!mIsLandscape && mCurrentTabPosition == ListsFragment.TAB_INDEX_SPEED_DIAL) ? 1259 FloatingActionButtonController.ALIGN_MIDDLE : 1260 FloatingActionButtonController.ALIGN_END; 1261 mFloatingActionButtonController.align(align, 0 /* offsetX */, 0 /* offsetY */, animate); 1262 } 1263} 1264