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