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