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