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 17 package com.android.dialer; 18 19 import android.animation.Animator; 20 import android.animation.Animator.AnimatorListener; 21 import android.animation.AnimatorListenerAdapter; 22 import android.app.Activity; 23 import android.app.Fragment; 24 import android.app.FragmentManager; 25 import android.app.FragmentTransaction; 26 import android.content.ActivityNotFoundException; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ResolveInfo; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.os.RemoteException; 34 import android.os.ServiceManager; 35 import android.provider.CallLog.Calls; 36 import android.provider.ContactsContract.Contacts; 37 import android.provider.ContactsContract.Intents; 38 import android.provider.ContactsContract.Intents.UI; 39 import android.speech.RecognizerIntent; 40 import android.telephony.TelephonyManager; 41 import android.text.Editable; 42 import android.text.TextUtils; 43 import android.text.TextWatcher; 44 import android.util.Log; 45 import android.view.Menu; 46 import android.view.MenuItem; 47 import android.view.View; 48 import android.view.ViewGroup.LayoutParams; 49 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 50 import android.view.inputmethod.InputMethodManager; 51 import android.widget.AbsListView.OnScrollListener; 52 import android.widget.EditText; 53 import android.widget.LinearLayout; 54 import android.widget.PopupMenu; 55 import android.widget.Toast; 56 57 import com.android.contacts.common.CallUtil; 58 import com.android.contacts.common.activity.TransactionSafeActivity; 59 import com.android.contacts.common.dialog.ClearFrequentsDialog; 60 import com.android.contacts.common.interactions.ImportExportDialogFragment; 61 import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; 62 import com.android.dialer.calllog.CallLogActivity; 63 import com.android.dialer.database.DialerDatabaseHelper; 64 import com.android.dialer.dialpad.DialpadFragment; 65 import com.android.dialer.dialpad.SmartDialNameMatcher; 66 import com.android.dialer.dialpad.SmartDialPrefix; 67 import com.android.dialer.interactions.PhoneNumberInteraction; 68 import com.android.dialer.list.AllContactsActivity; 69 import com.android.dialer.list.DragDropController; 70 import com.android.dialer.list.OnDragDropListener; 71 import com.android.dialer.list.OnListFragmentScrolledListener; 72 import com.android.dialer.list.PhoneFavoriteFragment; 73 import com.android.dialer.list.PhoneFavoriteTileView; 74 import com.android.dialer.list.RegularSearchFragment; 75 import com.android.dialer.list.RemoveView; 76 import com.android.dialer.list.SearchFragment; 77 import com.android.dialer.list.SmartDialSearchFragment; 78 import com.android.dialerbind.DatabaseHelperManager; 79 import com.android.internal.telephony.ITelephony; 80 81 import java.util.ArrayList; 82 import java.util.List; 83 84 /** 85 * The dialer tab's title is 'phone', a more common name (see strings.xml). 86 */ 87 public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener, 88 DialpadFragment.OnDialpadQueryChangedListener, PopupMenu.OnMenuItemClickListener, 89 OnListFragmentScrolledListener, 90 DialpadFragment.HostInterface, 91 PhoneFavoriteFragment.OnShowAllContactsListener, 92 PhoneFavoriteFragment.HostInterface, 93 OnDragDropListener, View.OnLongClickListener { 94 private static final String TAG = "DialtactsActivity"; 95 96 public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 97 98 public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences"; 99 100 /** Used to open Call Setting */ 101 private static final String PHONE_PACKAGE = "com.android.phone"; 102 private static final String CALL_SETTINGS_CLASS_NAME = 103 "com.android.phone.CallFeaturesSetting"; 104 /** @see #getCallOrigin() */ 105 private static final String CALL_ORIGIN_DIALTACTS = 106 "com.android.dialer.DialtactsActivity"; 107 108 private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui"; 109 private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui"; 110 private static final String KEY_SEARCH_QUERY = "search_query"; 111 private static final String KEY_FIRST_LAUNCH = "first_launch"; 112 113 private static final String TAG_DIALPAD_FRAGMENT = "dialpad"; 114 private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search"; 115 private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial"; 116 private static final String TAG_FAVORITES_FRAGMENT = "favorites"; 117 118 /** 119 * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}. 120 */ 121 private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER"; 122 123 private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1; 124 125 private static final int ANIMATION_DURATION = 200; 126 127 private String mFilterText; 128 129 /** 130 * The main fragment displaying the user's favorites and frequent contacts 131 */ 132 private PhoneFavoriteFragment mPhoneFavoriteFragment; 133 134 /** 135 * Fragment containing the dialpad that slides into view 136 */ 137 private DialpadFragment mDialpadFragment; 138 139 /** 140 * Fragment for searching phone numbers using the alphanumeric keyboard. 141 */ 142 private RegularSearchFragment mRegularSearchFragment; 143 144 /** 145 * Fragment for searching phone numbers using the dialpad. 146 */ 147 private SmartDialSearchFragment mSmartDialSearchFragment; 148 149 private View mMenuButton; 150 private View mFakeActionBar; 151 private View mCallHistoryButton; 152 private View mDialpadButton; 153 private View mDialButton; 154 private PopupMenu mOverflowMenu; 155 private PopupMenu mDialpadOverflowMenu; 156 157 // Padding view used to shift the fragment frame up when the dialpad is shown so that 158 // the contents of the fragment frame continue to exist in a layout of the same height 159 private View mFragmentsSpacer; 160 private View mFragmentsFrame; 161 162 private boolean mInDialpadSearch; 163 private boolean mInRegularSearch; 164 private boolean mClearSearchOnPause; 165 166 /** 167 * True if the dialpad is only temporarily showing due to being in call 168 */ 169 private boolean mInCallDialpadUp; 170 171 /** 172 * True when this activity has been launched for the first time. 173 */ 174 private boolean mFirstLaunch; 175 private View mSearchViewContainer; 176 private RemoveView mRemoveViewContainer; 177 // This view points to the Framelayout that houses both the search view and remove view 178 // containers. 179 private View mSearchAndRemoveViewContainer; 180 private View mSearchViewCloseButton; 181 private View mVoiceSearchButton; 182 private EditText mSearchView; 183 184 private String mSearchQuery; 185 186 private DialerDatabaseHelper mDialerDatabaseHelper; 187 188 private class OverflowPopupMenu extends PopupMenu { 189 public OverflowPopupMenu(Context context, View anchor) { 190 super(context, anchor); 191 } 192 193 @Override 194 public void show() { 195 final Menu menu = getMenu(); 196 final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents); 197 clearFrequents.setVisible(mPhoneFavoriteFragment.hasFrequents()); 198 super.show(); 199 } 200 } 201 202 /** 203 * Listener used when one of phone numbers in search UI is selected. This will initiate a 204 * phone call using the phone number. 205 */ 206 private final OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener = 207 new OnPhoneNumberPickerActionListener() { 208 @Override 209 public void onPickPhoneNumberAction(Uri dataUri) { 210 // Specify call-origin so that users will see the previous tab instead of 211 // CallLog screen (search UI will be automatically exited). 212 PhoneNumberInteraction.startInteractionForPhoneCall( 213 DialtactsActivity.this, dataUri, getCallOrigin()); 214 mClearSearchOnPause = true; 215 } 216 217 @Override 218 public void onCallNumberDirectly(String phoneNumber) { 219 Intent intent = CallUtil.getCallIntent(phoneNumber, getCallOrigin()); 220 startActivity(intent); 221 mClearSearchOnPause = true; 222 } 223 224 @Override 225 public void onShortcutIntentCreated(Intent intent) { 226 Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring."); 227 } 228 229 @Override 230 public void onHomeInActionBarSelected() { 231 exitSearchUi(); 232 } 233 }; 234 235 /** 236 * Listener used to send search queries to the phone search fragment. 237 */ 238 private final TextWatcher mPhoneSearchQueryTextListener = new TextWatcher() { 239 @Override 240 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 241 } 242 243 @Override 244 public void onTextChanged(CharSequence s, int start, int before, int count) { 245 final String newText = s.toString(); 246 if (newText.equals(mSearchQuery)) { 247 // If the query hasn't changed (perhaps due to activity being destroyed 248 // and restored, or user launching the same DIAL intent twice), then there is 249 // no need to do anything here. 250 return; 251 } 252 mSearchQuery = newText; 253 if (DEBUG) { 254 Log.d(TAG, "onTextChange for mSearchView called with new query: " + s); 255 } 256 final boolean dialpadSearch = isDialpadShowing(); 257 258 // Show search result with non-empty text. Show a bare list otherwise. 259 if (TextUtils.isEmpty(newText) && getInSearchUi()) { 260 exitSearchUi(); 261 mSearchViewCloseButton.setVisibility(View.GONE); 262 mVoiceSearchButton.setVisibility(View.VISIBLE); 263 return; 264 } else if (!TextUtils.isEmpty(newText)) { 265 final boolean sameSearchMode = (dialpadSearch && mInDialpadSearch) || 266 (!dialpadSearch && mInRegularSearch); 267 if (!sameSearchMode) { 268 // call enterSearchUi only if we are switching search modes, or entering 269 // search ui for the first time 270 enterSearchUi(dialpadSearch, newText); 271 } 272 273 if (dialpadSearch && mSmartDialSearchFragment != null) { 274 mSmartDialSearchFragment.setQueryString(newText, false); 275 } else if (mRegularSearchFragment != null) { 276 mRegularSearchFragment.setQueryString(newText, false); 277 } 278 mSearchViewCloseButton.setVisibility(View.VISIBLE); 279 mVoiceSearchButton.setVisibility(View.GONE); 280 return; 281 } 282 } 283 284 @Override 285 public void afterTextChanged(Editable s) { 286 } 287 }; 288 289 private boolean isDialpadShowing() { 290 return mDialpadFragment != null && mDialpadFragment.isVisible(); 291 } 292 293 @Override 294 protected void onCreate(Bundle savedInstanceState) { 295 super.onCreate(savedInstanceState); 296 mFirstLaunch = true; 297 298 final Intent intent = getIntent(); 299 fixIntent(intent); 300 301 setContentView(R.layout.dialtacts_activity); 302 303 getActionBar().hide(); 304 305 // Add the favorites fragment, and the dialpad fragment, but only if savedInstanceState 306 // is null. Otherwise the fragment manager takes care of recreating these fragments. 307 if (savedInstanceState == null) { 308 getFragmentManager().beginTransaction() 309 .add(R.id.dialtacts_frame, new PhoneFavoriteFragment(), TAG_FAVORITES_FRAGMENT) 310 .add(R.id.dialtacts_container, new DialpadFragment(), TAG_DIALPAD_FRAGMENT) 311 .commit(); 312 } else { 313 mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY); 314 mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI); 315 mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI); 316 mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH); 317 } 318 319 mFragmentsFrame = findViewById(R.id.dialtacts_frame); 320 mFragmentsSpacer = findViewById(R.id.contact_tile_frame_spacer); 321 322 mRemoveViewContainer = (RemoveView) findViewById(R.id.remove_view_container); 323 mSearchAndRemoveViewContainer = findViewById(R.id.search_and_remove_view_container); 324 325 // When the first global layout pass is completed (and mSearchAndRemoveViewContainer has 326 // been assigned a valid height), assign that height to mFragmentsSpacer as well. 327 mSearchAndRemoveViewContainer.getViewTreeObserver().addOnGlobalLayoutListener( 328 new OnGlobalLayoutListener() { 329 @Override 330 public void onGlobalLayout() { 331 mSearchAndRemoveViewContainer.getViewTreeObserver() 332 .removeOnGlobalLayoutListener(this); 333 mFragmentsSpacer.setLayoutParams( 334 new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 335 mSearchAndRemoveViewContainer.getHeight())); 336 } 337 }); 338 339 setupFakeActionBarItems(); 340 prepareSearchView(); 341 342 if (UI.FILTER_CONTACTS_ACTION.equals(intent.getAction()) 343 && savedInstanceState == null) { 344 setupFilterText(intent); 345 } 346 347 hideDialpadFragment(false, false); 348 349 mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this); 350 SmartDialPrefix.initializeNanpSettings(this); 351 } 352 353 @Override 354 protected void onResume() { 355 super.onResume(); 356 if (mFirstLaunch) { 357 displayFragment(getIntent()); 358 } else if (!phoneIsInUse() && mInCallDialpadUp) { 359 hideDialpadFragment(false, true); 360 mInCallDialpadUp = false; 361 } 362 prepareVoiceSearchButton(); 363 mFirstLaunch = false; 364 mDialerDatabaseHelper.startSmartDialUpdateThread(); 365 } 366 367 @Override 368 protected void onPause() { 369 if (mClearSearchOnPause) { 370 hideDialpadAndSearchUi(); 371 mClearSearchOnPause = false; 372 } 373 super.onPause(); 374 } 375 376 @Override 377 protected void onSaveInstanceState(Bundle outState) { 378 super.onSaveInstanceState(outState); 379 outState.putString(KEY_SEARCH_QUERY, mSearchQuery); 380 outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch); 381 outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch); 382 outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch); 383 } 384 385 @Override 386 public void onAttachFragment(Fragment fragment) { 387 if (fragment instanceof DialpadFragment) { 388 mDialpadFragment = (DialpadFragment) fragment; 389 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 390 transaction.hide(mDialpadFragment); 391 transaction.commit(); 392 } else if (fragment instanceof SmartDialSearchFragment) { 393 mSmartDialSearchFragment = (SmartDialSearchFragment) fragment; 394 mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener( 395 mPhoneNumberPickerActionListener); 396 if (mFragmentsFrame != null) { 397 mFragmentsFrame.setAlpha(1.0f); 398 } 399 } else if (fragment instanceof SearchFragment) { 400 mRegularSearchFragment = (RegularSearchFragment) fragment; 401 mRegularSearchFragment.setOnPhoneNumberPickerActionListener( 402 mPhoneNumberPickerActionListener); 403 if (mFragmentsFrame != null) { 404 mFragmentsFrame.setAlpha(1.0f); 405 } 406 } else if (fragment instanceof PhoneFavoriteFragment) { 407 mPhoneFavoriteFragment = (PhoneFavoriteFragment) fragment; 408 mPhoneFavoriteFragment.setListener(mPhoneFavoriteListener); 409 } 410 } 411 412 @Override 413 public boolean onMenuItemClick(MenuItem item) { 414 switch (item.getItemId()) { 415 case R.id.menu_import_export: 416 // We hard-code the "contactsAreAvailable" argument because doing it properly would 417 // involve querying a {@link ProviderStatusLoader}, which we don't want to do right 418 // now in Dialtacts for (potential) performance reasons. Compare with how it is 419 // done in {@link PeopleActivity}. 420 ImportExportDialogFragment.show(getFragmentManager(), true, 421 DialtactsActivity.class); 422 return true; 423 case R.id.menu_clear_frequents: 424 ClearFrequentsDialog.show(getFragmentManager()); 425 return true; 426 case R.id.menu_add_contact: 427 try { 428 startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI)); 429 } catch (ActivityNotFoundException e) { 430 Toast toast = Toast.makeText(this, 431 R.string.add_contact_not_available, 432 Toast.LENGTH_SHORT); 433 toast.show(); 434 } 435 return true; 436 case R.id.menu_call_settings: 437 handleMenuSettings(); 438 return true; 439 case R.id.menu_all_contacts: 440 onShowAllContacts(); 441 return true; 442 } 443 return false; 444 } 445 446 protected void handleMenuSettings() { 447 openTelephonySetting(this); 448 } 449 450 public static void openTelephonySetting(Activity activity) { 451 final Intent settingsIntent = getCallSettingsIntent(); 452 activity.startActivity(settingsIntent); 453 } 454 455 @Override 456 public void onClick(View view) { 457 switch (view.getId()) { 458 case R.id.overflow_menu: { 459 if (isDialpadShowing()) { 460 mDialpadOverflowMenu.show(); 461 } else { 462 mOverflowMenu.show(); 463 } 464 break; 465 } 466 case R.id.dialpad_button: 467 // Reset the boolean flag that tracks whether the dialpad was up because 468 // we were in call. Regardless of whether it was true before, we want to 469 // show the dialpad because the user has explicitly clicked the dialpad 470 // button. 471 mInCallDialpadUp = false; 472 showDialpadFragment(true); 473 break; 474 case R.id.dial_button: 475 // Dial button was pressed; tell the Dialpad fragment 476 mDialpadFragment.dialButtonPressed(); 477 break; 478 case R.id.call_history_button: 479 // Use explicit CallLogActivity intent instead of ACTION_VIEW + 480 // CONTENT_TYPE, so that we always open our call log from our dialer 481 final Intent intent = new Intent(this, CallLogActivity.class); 482 startActivity(intent); 483 break; 484 case R.id.search_close_button: 485 // Clear the search field 486 if (!TextUtils.isEmpty(mSearchView.getText())) { 487 mDialpadFragment.clearDialpad(); 488 mSearchView.setText(""); 489 } 490 break; 491 case R.id.voice_search_button: 492 try { 493 startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 494 ACTIVITY_REQUEST_CODE_VOICE_SEARCH); 495 } catch (ActivityNotFoundException e) { 496 Toast.makeText(DialtactsActivity.this, R.string.voice_search_not_available, 497 Toast.LENGTH_SHORT).show(); 498 } 499 break; 500 default: { 501 Log.wtf(TAG, "Unexpected onClick event from " + view); 502 break; 503 } 504 } 505 } 506 507 @Override 508 public boolean onLongClick(View view) { 509 switch (view.getId()) { 510 case R.id.dial_button: { 511 // Dial button was pressed; tell the Dialpad fragment 512 mDialpadFragment.dialButtonPressed(); 513 return true; // Consume the event 514 } 515 default: { 516 Log.wtf(TAG, "Unexpected onClick event from " + view); 517 break; 518 } 519 } 520 return false; 521 } 522 523 @Override 524 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 525 if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) { 526 if (resultCode == RESULT_OK) { 527 final ArrayList<String> matches = data.getStringArrayListExtra( 528 RecognizerIntent.EXTRA_RESULTS); 529 if (matches.size() > 0) { 530 final String match = matches.get(0); 531 mSearchView.setText(match); 532 } else { 533 Log.e(TAG, "Voice search - nothing heard"); 534 } 535 } else { 536 Log.e(TAG, "Voice search failed"); 537 } 538 } 539 super.onActivityResult(requestCode, resultCode, data); 540 } 541 542 private void showDialpadFragment(boolean animate) { 543 mDialpadFragment.setAdjustTranslationForAnimation(animate); 544 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 545 if (animate) { 546 ft.setCustomAnimations(R.anim.slide_in, 0); 547 } else { 548 mDialpadFragment.setYFraction(0); 549 } 550 ft.show(mDialpadFragment); 551 ft.commit(); 552 mDialButton.setVisibility(shouldShowOnscreenDialButton() ? View.VISIBLE : View.GONE); 553 mDialpadButton.setVisibility(View.GONE); 554 555 if (mDialpadOverflowMenu == null) { 556 mDialpadOverflowMenu = mDialpadFragment.buildOptionsMenu(mMenuButton); 557 } 558 559 mMenuButton.setOnTouchListener(mDialpadOverflowMenu.getDragToOpenListener()); 560 } 561 562 public void hideDialpadFragment(boolean animate, boolean clearDialpad) { 563 if (mDialpadFragment == null) return; 564 if (clearDialpad) { 565 mDialpadFragment.clearDialpad(); 566 } 567 if (!mDialpadFragment.isVisible()) return; 568 mDialpadFragment.setAdjustTranslationForAnimation(animate); 569 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 570 if (animate) { 571 ft.setCustomAnimations(0, R.anim.slide_out); 572 } 573 ft.hide(mDialpadFragment); 574 ft.commit(); 575 mDialButton.setVisibility(View.GONE); 576 mDialpadButton.setVisibility(View.VISIBLE); 577 mMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener()); 578 } 579 580 private void prepareSearchView() { 581 mSearchViewContainer = findViewById(R.id.search_view_container); 582 mSearchViewCloseButton = findViewById(R.id.search_close_button); 583 mSearchViewCloseButton.setOnClickListener(this); 584 585 mSearchView = (EditText) findViewById(R.id.search_view); 586 mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); 587 mSearchView.setHint(getString(R.string.dialer_hint_find_contact)); 588 589 prepareVoiceSearchButton(); 590 } 591 592 private void prepareVoiceSearchButton() { 593 mVoiceSearchButton = findViewById(R.id.voice_search_button); 594 final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 595 if (canIntentBeHandled(voiceIntent)) { 596 mVoiceSearchButton.setVisibility(View.VISIBLE); 597 mVoiceSearchButton.setOnClickListener(this); 598 } else { 599 mVoiceSearchButton.setVisibility(View.GONE); 600 } 601 } 602 603 final AnimatorListener mHideListener = new AnimatorListenerAdapter() { 604 @Override 605 public void onAnimationEnd(Animator animation) { 606 mSearchAndRemoveViewContainer.setVisibility(View.GONE); 607 } 608 }; 609 610 private boolean getInSearchUi() { 611 return mInDialpadSearch || mInRegularSearch; 612 } 613 614 private void setNotInSearchUi() { 615 mInDialpadSearch = false; 616 mInRegularSearch = false; 617 } 618 619 private void hideDialpadAndSearchUi() { 620 mSearchView.setText(null); 621 hideDialpadFragment(false, true); 622 } 623 624 public void hideSearchBar() { 625 final int height = mSearchAndRemoveViewContainer.getHeight(); 626 627 mSearchAndRemoveViewContainer.animate().cancel(); 628 mSearchAndRemoveViewContainer.setAlpha(1); 629 mSearchAndRemoveViewContainer.setTranslationY(0); 630 mSearchAndRemoveViewContainer.animate().withLayer().alpha(0) 631 .translationY(-height).setDuration(ANIMATION_DURATION) 632 .setListener(mHideListener); 633 634 mFragmentsFrame.animate().withLayer() 635 .translationY(-height).setDuration(ANIMATION_DURATION).setListener( 636 new AnimatorListenerAdapter() { 637 @Override 638 public void onAnimationEnd(Animator animation) { 639 mFragmentsFrame.setTranslationY(0); 640 // Display the fragments spacer (which has the same height as the 641 // search box) now that the search box is hidden, so that 642 // mFragmentsFrame always retains the same height 643 mFragmentsSpacer.setVisibility(View.VISIBLE); 644 } 645 }); 646 647 if (!mInDialpadSearch && !mInRegularSearch) { 648 // If the favorites fragment is showing, fade to blank. 649 mFragmentsFrame.animate().alpha(0.0f); 650 } 651 } 652 653 public void showSearchBar() { 654 final int height = mSearchAndRemoveViewContainer.getHeight(); 655 mSearchAndRemoveViewContainer.animate().cancel(); 656 mSearchAndRemoveViewContainer.setAlpha(0); 657 mSearchAndRemoveViewContainer.setTranslationY(-height); 658 mSearchAndRemoveViewContainer.animate().withLayer().alpha(1).translationY(0) 659 .setDuration(ANIMATION_DURATION).setListener(new AnimatorListenerAdapter() { 660 @Override 661 public void onAnimationStart(Animator animation) { 662 mSearchAndRemoveViewContainer.setVisibility(View.VISIBLE); 663 } 664 }); 665 666 mFragmentsFrame.setTranslationY(-height); 667 mFragmentsFrame.animate().withLayer().translationY(0).setDuration(ANIMATION_DURATION) 668 .alpha(1.0f) 669 .setListener( 670 new AnimatorListenerAdapter() { 671 @Override 672 public void onAnimationStart(Animator animation) { 673 // Hide the fragment spacer now that the search box will 674 // be displayed again 675 mFragmentsSpacer.setVisibility(View.GONE); 676 } 677 }); 678 } 679 680 private void setupFakeActionBarItems() { 681 mMenuButton = findViewById(R.id.overflow_menu); 682 if (mMenuButton != null) { 683 mMenuButton.setOnClickListener(this); 684 if (mOverflowMenu == null) { 685 mOverflowMenu = buildOptionsMenu(mMenuButton); 686 } 687 mMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener()); 688 } 689 690 mFakeActionBar = findViewById(R.id.fake_action_bar); 691 692 mCallHistoryButton = findViewById(R.id.call_history_button); 693 mCallHistoryButton.setOnClickListener(this); 694 695 mDialButton = findViewById(R.id.dial_button); 696 mDialButton.setOnClickListener(this); 697 mDialButton.setOnLongClickListener(this); 698 699 mDialpadButton = findViewById(R.id.dialpad_button); 700 mDialpadButton.setOnClickListener(this); 701 } 702 703 private PopupMenu buildOptionsMenu(View invoker) { 704 PopupMenu menu = new OverflowPopupMenu(this, invoker); 705 menu.inflate(R.menu.dialtacts_options); 706 menu.setOnMenuItemClickListener(this); 707 return menu; 708 } 709 710 private void fixIntent(Intent intent) { 711 // This should be cleaned up: the call key used to send an Intent 712 // that just said to go to the recent calls list. It now sends this 713 // abstract action, but this class hasn't been rewritten to deal with it. 714 if (Intent.ACTION_CALL_BUTTON.equals(intent.getAction())) { 715 intent.setDataAndType(Calls.CONTENT_URI, Calls.CONTENT_TYPE); 716 intent.putExtra("call_key", true); 717 setIntent(intent); 718 } 719 } 720 721 /** 722 * Returns true if the intent is due to hitting the green send key (hardware call button: 723 * KEYCODE_CALL) while in a call. 724 * 725 * @param intent the intent that launched this activity 726 * @param recentCallsRequest true if the intent is requesting to view recent calls 727 * @return true if the intent is due to hitting the green send key while in a call 728 */ 729 private boolean isSendKeyWhileInCall(Intent intent, boolean recentCallsRequest) { 730 // If there is a call in progress go to the call screen 731 if (recentCallsRequest) { 732 final boolean callKey = intent.getBooleanExtra("call_key", false); 733 734 try { 735 ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); 736 if (callKey && phone != null && phone.showCallScreen()) { 737 return true; 738 } 739 } catch (RemoteException e) { 740 Log.e(TAG, "Failed to handle send while in call", e); 741 } 742 } 743 744 return false; 745 } 746 747 /** 748 * Sets the current tab based on the intent's request type 749 * 750 * @param intent Intent that contains information about which tab should be selected 751 */ 752 private void displayFragment(Intent intent) { 753 // If we got here by hitting send and we're in call forward along to the in-call activity 754 boolean recentCallsRequest = Calls.CONTENT_TYPE.equals(intent.resolveType( 755 getContentResolver())); 756 if (isSendKeyWhileInCall(intent, recentCallsRequest)) { 757 finish(); 758 return; 759 } 760 761 if (mDialpadFragment != null) { 762 final boolean phoneIsInUse = phoneIsInUse(); 763 if (phoneIsInUse || isDialIntent(intent)) { 764 mDialpadFragment.setStartedFromNewIntent(true); 765 if (phoneIsInUse && !mDialpadFragment.isVisible()) { 766 mInCallDialpadUp = true; 767 } 768 showDialpadFragment(false); 769 } 770 } 771 } 772 773 @Override 774 public void onNewIntent(Intent newIntent) { 775 setIntent(newIntent); 776 fixIntent(newIntent); 777 displayFragment(newIntent); 778 final String action = newIntent.getAction(); 779 780 invalidateOptionsMenu(); 781 } 782 783 /** Returns true if the given intent contains a phone number to populate the dialer with */ 784 private boolean isDialIntent(Intent intent) { 785 final String action = intent.getAction(); 786 if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) { 787 return true; 788 } 789 if (Intent.ACTION_VIEW.equals(action)) { 790 final Uri data = intent.getData(); 791 if (data != null && CallUtil.SCHEME_TEL.equals(data.getScheme())) { 792 return true; 793 } 794 } 795 return false; 796 } 797 798 /** 799 * Returns an appropriate call origin for this Activity. May return null when no call origin 800 * should be used (e.g. when some 3rd party application launched the screen. Call origin is 801 * for remembering the tab in which the user made a phone call, so the external app's DIAL 802 * request should not be counted.) 803 */ 804 public String getCallOrigin() { 805 return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null; 806 } 807 808 /** 809 * Retrieves the filter text stored in {@link #setupFilterText(Intent)}. 810 * This text originally came from a FILTER_CONTACTS_ACTION intent received 811 * by this activity. The stored text will then be cleared after after this 812 * method returns. 813 * 814 * @return The stored filter text 815 */ 816 public String getAndClearFilterText() { 817 String filterText = mFilterText; 818 mFilterText = null; 819 return filterText; 820 } 821 822 /** 823 * Stores the filter text associated with a FILTER_CONTACTS_ACTION intent. 824 * This is so child activities can check if they are supposed to display a filter. 825 * 826 * @param intent The intent received in {@link #onNewIntent(Intent)} 827 */ 828 private void setupFilterText(Intent intent) { 829 // If the intent was relaunched from history, don't apply the filter text. 830 if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { 831 return; 832 } 833 String filter = intent.getStringExtra(UI.FILTER_TEXT_EXTRA_KEY); 834 if (filter != null && filter.length() > 0) { 835 mFilterText = filter; 836 } 837 } 838 839 private final PhoneFavoriteFragment.Listener mPhoneFavoriteListener = 840 new PhoneFavoriteFragment.Listener() { 841 @Override 842 public void onContactSelected(Uri contactUri) { 843 PhoneNumberInteraction.startInteractionForPhoneCall( 844 DialtactsActivity.this, contactUri, getCallOrigin()); 845 } 846 847 @Override 848 public void onCallNumberDirectly(String phoneNumber) { 849 Intent intent = CallUtil.getCallIntent(phoneNumber, getCallOrigin()); 850 startActivity(intent); 851 } 852 }; 853 854 /* TODO krelease: This is only relevant for phones that have a hard button search key (i.e. 855 * Nexus S). Supporting it is a little more tricky because of the dialpad fragment might 856 * be showing when the search key is pressed so there is more state management involved. 857 858 @Override 859 public void startSearch(String initialQuery, boolean selectInitialQuery, 860 Bundle appSearchData, boolean globalSearch) { 861 if (mRegularSearchFragment != null && mRegularSearchFragment.isAdded() && !globalSearch) { 862 if (mInSearchUi) { 863 if (mSearchView.hasFocus()) { 864 showInputMethod(mSearchView.findFocus()); 865 } else { 866 mSearchView.requestFocus(); 867 } 868 } else { 869 enterSearchUi(); 870 } 871 } else { 872 super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); 873 } 874 }*/ 875 876 private void showInputMethod(View view) { 877 final InputMethodManager imm = (InputMethodManager) getSystemService( 878 Context.INPUT_METHOD_SERVICE); 879 if (imm != null) { 880 imm.showSoftInput(view, 0); 881 } 882 } 883 884 private void hideInputMethod(View view) { 885 final InputMethodManager imm = (InputMethodManager) getSystemService( 886 Context.INPUT_METHOD_SERVICE); 887 if (imm != null && view != null) { 888 imm.hideSoftInputFromWindow(view.getWindowToken(), 0); 889 } 890 } 891 892 /** 893 * Shows the search fragment 894 */ 895 private void enterSearchUi(boolean smartDialSearch, String query) { 896 if (getFragmentManager().isDestroyed()) { 897 // Weird race condition where fragment is doing work after the activity is destroyed 898 // due to talkback being on (b/10209937). Just return since we can't do any 899 // constructive here. 900 return; 901 } 902 903 if (DEBUG) { 904 Log.d(TAG, "Entering search UI - smart dial " + smartDialSearch); 905 } 906 907 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 908 909 SearchFragment fragment; 910 if (mInDialpadSearch) { 911 transaction.remove(mSmartDialSearchFragment); 912 } else if (mInRegularSearch) { 913 transaction.remove(mRegularSearchFragment); 914 } else { 915 transaction.remove(mPhoneFavoriteFragment); 916 } 917 918 final String tag; 919 if (smartDialSearch) { 920 tag = TAG_SMARTDIAL_SEARCH_FRAGMENT; 921 } else { 922 tag = TAG_REGULAR_SEARCH_FRAGMENT; 923 } 924 mInDialpadSearch = smartDialSearch; 925 mInRegularSearch = !smartDialSearch; 926 927 fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag); 928 if (fragment == null) { 929 if (smartDialSearch) { 930 fragment = new SmartDialSearchFragment(); 931 } else { 932 fragment = new RegularSearchFragment(); 933 } 934 } 935 transaction.replace(R.id.dialtacts_frame, fragment, tag); 936 transaction.addToBackStack(null); 937 fragment.setQueryString(query, false); 938 transaction.commit(); 939 } 940 941 /** 942 * Hides the search fragment 943 */ 944 private void exitSearchUi() { 945 // See related bug in enterSearchUI(); 946 if (getFragmentManager().isDestroyed()) { 947 return; 948 } 949 // Go all the way back to the favorites fragment, regardless of how many times we 950 // transitioned between search fragments 951 getFragmentManager().popBackStack(0, FragmentManager.POP_BACK_STACK_INCLUSIVE); 952 setNotInSearchUi(); 953 954 if (isDialpadShowing()) { 955 mFragmentsFrame.setAlpha(0); 956 } 957 } 958 959 /** Returns an Intent to launch Call Settings screen */ 960 public static Intent getCallSettingsIntent() { 961 final Intent intent = new Intent(Intent.ACTION_MAIN); 962 intent.setClassName(PHONE_PACKAGE, CALL_SETTINGS_CLASS_NAME); 963 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 964 return intent; 965 } 966 967 @Override 968 public void onBackPressed() { 969 if (mDialpadFragment != null && mDialpadFragment.isVisible()) { 970 hideDialpadFragment(true, false); 971 } else if (getInSearchUi()) { 972 mSearchView.setText(null); 973 mDialpadFragment.clearDialpad(); 974 } else { 975 super.onBackPressed(); 976 } 977 } 978 979 @Override 980 public void onDialpadQueryChanged(String query) { 981 final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query, 982 SmartDialNameMatcher.LATIN_SMART_DIAL_MAP); 983 if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) { 984 if (DEBUG) { 985 Log.d(TAG, "onDialpadQueryChanged - new query: " + query); 986 } 987 if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { 988 // This callback can happen if the dialpad fragment is recreated because of 989 // activity destruction. In that case, don't update the search view because 990 // that would bring the user back to the search fragment regardless of the 991 // previous state of the application. Instead, just return here and let the 992 // fragment manager correctly figure out whatever fragment was last displayed. 993 return; 994 } 995 mSearchView.setText(normalizedQuery); 996 } 997 } 998 999 @Override 1000 public void onListFragmentScrollStateChange(int scrollState) { 1001 if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 1002 hideDialpadFragment(true, false); 1003 hideInputMethod(getCurrentFocus()); 1004 } 1005 } 1006 1007 @Override 1008 public void setDialButtonEnabled(boolean enabled) { 1009 if (mDialButton != null) { 1010 mDialButton.setEnabled(enabled); 1011 } 1012 } 1013 1014 @Override 1015 public void setDialButtonContainerVisible(boolean visible) { 1016 mFakeActionBar.setVisibility(visible ? View.VISIBLE : View.GONE); 1017 } 1018 1019 private boolean phoneIsInUse() { 1020 final TelephonyManager tm = (TelephonyManager) getSystemService( 1021 Context.TELEPHONY_SERVICE); 1022 return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE; 1023 } 1024 1025 @Override 1026 public void onShowAllContacts() { 1027 final Intent intent = new Intent(this, AllContactsActivity.class); 1028 startActivity(intent); 1029 } 1030 1031 public static Intent getAddNumberToContactIntent(CharSequence text) { 1032 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 1033 intent.putExtra(Intents.Insert.PHONE, text); 1034 intent.setType(Contacts.CONTENT_ITEM_TYPE); 1035 return intent; 1036 } 1037 1038 private boolean canIntentBeHandled(Intent intent) { 1039 final PackageManager packageManager = getPackageManager(); 1040 final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent, 1041 PackageManager.MATCH_DEFAULT_ONLY); 1042 return resolveInfo != null && resolveInfo.size() > 0; 1043 } 1044 1045 @Override 1046 public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView view) { 1047 crossfadeViews(mRemoveViewContainer, mSearchViewContainer, ANIMATION_DURATION); 1048 } 1049 1050 @Override 1051 public void onDragHovered(int itemIndex, int x, int y) {} 1052 1053 @Override 1054 public void onDragFinished(int x, int y) { 1055 crossfadeViews(mSearchViewContainer, mRemoveViewContainer, ANIMATION_DURATION); 1056 } 1057 1058 @Override 1059 public void onDroppedOnRemove() {} 1060 1061 /** 1062 * Allows the PhoneFavoriteFragment to attach the drag controller to mRemoveViewContainer 1063 * once it has been attached to the activity. 1064 */ 1065 @Override 1066 public void setDragDropController(DragDropController dragController) { 1067 mRemoveViewContainer.setDragDropController(dragController); 1068 } 1069 1070 /** 1071 * Crossfades two views so that the first one appears while the other one is fading 1072 * out of view. 1073 */ 1074 private void crossfadeViews(final View fadeIn, final View fadeOut, int duration) { 1075 fadeOut.animate().alpha(0).setDuration(duration) 1076 .setListener(new AnimatorListenerAdapter() { 1077 @Override 1078 public void onAnimationEnd(Animator animation) { 1079 fadeOut.setVisibility(View.GONE); 1080 } 1081 }); 1082 1083 fadeIn.setVisibility(View.VISIBLE); 1084 fadeIn.setAlpha(0); 1085 fadeIn.animate().alpha(1).setDuration(ANIMATION_DURATION) 1086 .setListener(null); 1087 } 1088 1089 private boolean shouldShowOnscreenDialButton() { 1090 return getResources().getBoolean(R.bool.config_show_onscreen_dial_button); 1091 } 1092 } 1093