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.app; 18 19 import android.app.Fragment; 20 import android.app.FragmentTransaction; 21 import android.app.KeyguardManager; 22 import android.content.ActivityNotFoundException; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.content.res.Configuration; 28 import android.content.res.Resources; 29 import android.database.Cursor; 30 import android.net.Uri; 31 import android.os.Bundle; 32 import android.os.SystemClock; 33 import android.os.Trace; 34 import android.provider.CallLog.Calls; 35 import android.speech.RecognizerIntent; 36 import android.support.annotation.MainThread; 37 import android.support.annotation.NonNull; 38 import android.support.annotation.VisibleForTesting; 39 import android.support.design.widget.CoordinatorLayout; 40 import android.support.design.widget.FloatingActionButton; 41 import android.support.design.widget.Snackbar; 42 import android.support.v4.app.ActivityCompat; 43 import android.support.v4.view.ViewPager; 44 import android.support.v7.app.ActionBar; 45 import android.telecom.PhoneAccount; 46 import android.text.Editable; 47 import android.text.TextUtils; 48 import android.text.TextWatcher; 49 import android.view.DragEvent; 50 import android.view.Gravity; 51 import android.view.KeyEvent; 52 import android.view.Menu; 53 import android.view.MenuItem; 54 import android.view.MotionEvent; 55 import android.view.View; 56 import android.view.View.OnDragListener; 57 import android.view.animation.Animation; 58 import android.view.animation.AnimationUtils; 59 import android.widget.AbsListView.OnScrollListener; 60 import android.widget.EditText; 61 import android.widget.ImageButton; 62 import android.widget.PopupMenu; 63 import android.widget.TextView; 64 import android.widget.Toast; 65 import com.android.contacts.common.dialog.ClearFrequentsDialog; 66 import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; 67 import com.android.contacts.common.list.PhoneNumberListAdapter; 68 import com.android.contacts.common.list.PhoneNumberPickerFragment.CursorReranker; 69 import com.android.contacts.common.list.PhoneNumberPickerFragment.OnLoadFinishedListener; 70 import com.android.contacts.common.widget.FloatingActionButtonController; 71 import com.android.dialer.animation.AnimUtils; 72 import com.android.dialer.animation.AnimationListenerAdapter; 73 import com.android.dialer.app.calllog.CallLogActivity; 74 import com.android.dialer.app.calllog.CallLogAdapter; 75 import com.android.dialer.app.calllog.CallLogFragment; 76 import com.android.dialer.app.calllog.CallLogNotificationsService; 77 import com.android.dialer.app.calllog.IntentProvider; 78 import com.android.dialer.app.dialpad.DialpadFragment; 79 import com.android.dialer.app.list.DialtactsPagerAdapter; 80 import com.android.dialer.app.list.DialtactsPagerAdapter.TabIndex; 81 import com.android.dialer.app.list.DragDropController; 82 import com.android.dialer.app.list.ListsFragment; 83 import com.android.dialer.app.list.OldSpeedDialFragment; 84 import com.android.dialer.app.list.OnDragDropListener; 85 import com.android.dialer.app.list.OnListFragmentScrolledListener; 86 import com.android.dialer.app.list.PhoneFavoriteSquareTileView; 87 import com.android.dialer.app.list.RegularSearchFragment; 88 import com.android.dialer.app.list.SearchFragment; 89 import com.android.dialer.app.list.SmartDialSearchFragment; 90 import com.android.dialer.app.settings.DialerSettingsActivity; 91 import com.android.dialer.app.widget.ActionBarController; 92 import com.android.dialer.app.widget.SearchEditTextLayout; 93 import com.android.dialer.callcomposer.CallComposerActivity; 94 import com.android.dialer.calldetails.CallDetailsActivity; 95 import com.android.dialer.callintent.CallIntentBuilder; 96 import com.android.dialer.callintent.CallSpecificAppData; 97 import com.android.dialer.common.Assert; 98 import com.android.dialer.common.LogUtil; 99 import com.android.dialer.configprovider.ConfigProviderBindings; 100 import com.android.dialer.database.Database; 101 import com.android.dialer.database.DialerDatabaseHelper; 102 import com.android.dialer.interactions.PhoneNumberInteraction; 103 import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorCode; 104 import com.android.dialer.logging.DialerImpression; 105 import com.android.dialer.logging.Logger; 106 import com.android.dialer.logging.ScreenEvent; 107 import com.android.dialer.logging.UiAction; 108 import com.android.dialer.main.Main; 109 import com.android.dialer.main.MainComponent; 110 import com.android.dialer.p13n.inference.P13nRanking; 111 import com.android.dialer.p13n.inference.protocol.P13nRanker; 112 import com.android.dialer.p13n.inference.protocol.P13nRanker.P13nRefreshCompleteListener; 113 import com.android.dialer.p13n.logging.P13nLogger; 114 import com.android.dialer.p13n.logging.P13nLogging; 115 import com.android.dialer.performancereport.PerformanceReport; 116 import com.android.dialer.postcall.PostCall; 117 import com.android.dialer.proguard.UsedByReflection; 118 import com.android.dialer.searchfragment.list.NewSearchFragment; 119 import com.android.dialer.simulator.Simulator; 120 import com.android.dialer.simulator.SimulatorComponent; 121 import com.android.dialer.smartdial.SmartDialNameMatcher; 122 import com.android.dialer.smartdial.SmartDialPrefix; 123 import com.android.dialer.telecom.TelecomUtil; 124 import com.android.dialer.util.DialerUtils; 125 import com.android.dialer.util.PermissionsUtil; 126 import com.android.dialer.util.TouchPointManager; 127 import com.android.dialer.util.TransactionSafeActivity; 128 import com.android.dialer.util.ViewUtil; 129 import java.util.ArrayList; 130 import java.util.Arrays; 131 import java.util.List; 132 import java.util.Locale; 133 import java.util.concurrent.TimeUnit; 134 135 /** The dialer tab's title is 'phone', a more common name (see strings.xml). */ 136 @UsedByReflection(value = "AndroidManifest-app.xml") 137 public class DialtactsActivity extends TransactionSafeActivity 138 implements View.OnClickListener, 139 DialpadFragment.OnDialpadQueryChangedListener, 140 OnListFragmentScrolledListener, 141 CallLogFragment.HostInterface, 142 CallLogAdapter.OnActionModeStateChangedListener, 143 DialpadFragment.HostInterface, 144 OldSpeedDialFragment.HostInterface, 145 SearchFragment.HostInterface, 146 OnDragDropListener, 147 OnPhoneNumberPickerActionListener, 148 PopupMenu.OnMenuItemClickListener, 149 ViewPager.OnPageChangeListener, 150 ActionBarController.ActivityUi, 151 PhoneNumberInteraction.InteractionErrorListener, 152 PhoneNumberInteraction.DisambigDialogDismissedListener, 153 ActivityCompat.OnRequestPermissionsResultCallback { 154 155 public static final boolean DEBUG = false; 156 @VisibleForTesting public static final String TAG_DIALPAD_FRAGMENT = "dialpad"; 157 private static final String ACTION_SHOW_TAB = "ACTION_SHOW_TAB"; 158 @VisibleForTesting public static final String EXTRA_SHOW_TAB = "EXTRA_SHOW_TAB"; 159 public static final String EXTRA_CLEAR_NEW_VOICEMAILS = "EXTRA_CLEAR_NEW_VOICEMAILS"; 160 private static final String KEY_LAST_TAB = "last_tab"; 161 private static final String TAG = "DialtactsActivity"; 162 private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui"; 163 private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui"; 164 private static final String KEY_SEARCH_QUERY = "search_query"; 165 private static final String KEY_FIRST_LAUNCH = "first_launch"; 166 private static final String KEY_WAS_CONFIGURATION_CHANGE = "was_configuration_change"; 167 private static final String KEY_IS_DIALPAD_SHOWN = "is_dialpad_shown"; 168 private static final String TAG_NEW_SEARCH_FRAGMENT = "new_search"; 169 private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search"; 170 private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial"; 171 private static final String TAG_FAVORITES_FRAGMENT = "favorites"; 172 /** Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}. */ 173 private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER"; 174 175 private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1; 176 public static final int ACTIVITY_REQUEST_CODE_CALL_COMPOSE = 2; 177 public static final int ACTIVITY_REQUEST_CODE_LIGHTBRINGER = 3; 178 public static final int ACTIVITY_REQUEST_CODE_CALL_DETAILS = 4; 179 180 private static final int FAB_SCALE_IN_DELAY_MS = 300; 181 182 /** 183 * Minimum time the history tab must have been selected for it to be marked as seen in onStop() 184 */ 185 private static final long HISTORY_TAB_SEEN_TIMEOUT = TimeUnit.SECONDS.toMillis(3); 186 187 /** Fragment containing the dialpad that slides into view */ 188 protected DialpadFragment mDialpadFragment; 189 190 private CoordinatorLayout mParentLayout; 191 /** Fragment for searching phone numbers using the alphanumeric keyboard. */ 192 private RegularSearchFragment mRegularSearchFragment; 193 194 /** Fragment for searching phone numbers using the dialpad. */ 195 private SmartDialSearchFragment mSmartDialSearchFragment; 196 197 /** new Fragment for search phone numbers using the keyboard and the dialpad. */ 198 private NewSearchFragment mNewSearchFragment; 199 200 /** Animation that slides in. */ 201 private Animation mSlideIn; 202 203 /** Animation that slides out. */ 204 private Animation mSlideOut; 205 /** Fragment containing the speed dial list, call history list, and all contacts list. */ 206 private ListsFragment mListsFragment; 207 /** 208 * Tracks whether onSaveInstanceState has been called. If true, no fragment transactions can be 209 * commited. 210 */ 211 private boolean mStateSaved; 212 213 private boolean mIsRestarting; 214 private boolean mInDialpadSearch; 215 private boolean mInRegularSearch; 216 private boolean mClearSearchOnPause; 217 private boolean mIsDialpadShown; 218 private boolean mShowDialpadOnResume; 219 /** Whether or not the device is in landscape orientation. */ 220 private boolean mIsLandscape; 221 /** True if the dialpad is only temporarily showing due to being in call */ 222 private boolean mInCallDialpadUp; 223 /** True when this activity has been launched for the first time. */ 224 private boolean mFirstLaunch; 225 /** 226 * Search query to be applied to the SearchView in the ActionBar once onCreateOptionsMenu has been 227 * called. 228 */ 229 private String mPendingSearchViewQuery; 230 231 private PopupMenu mOverflowMenu; 232 private EditText mSearchView; 233 private View mVoiceSearchButton; 234 private String mSearchQuery; 235 private String mDialpadQuery; 236 private DialerDatabaseHelper mDialerDatabaseHelper; 237 private DragDropController mDragDropController; 238 private ActionBarController mActionBarController; 239 private FloatingActionButtonController mFloatingActionButtonController; 240 private boolean mWasConfigurationChange; 241 private long timeTabSelected; 242 243 private P13nLogger mP13nLogger; 244 private P13nRanker mP13nRanker; 245 public boolean isMultiSelectModeEnabled; 246 247 private boolean isLastTabEnabled; 248 249 AnimationListenerAdapter mSlideInListener = 250 new AnimationListenerAdapter() { 251 @Override 252 public void onAnimationEnd(Animation animation) { 253 maybeEnterSearchUi(); 254 } 255 }; 256 /** Listener for after slide out animation completes on dialer fragment. */ 257 AnimationListenerAdapter mSlideOutListener = 258 new AnimationListenerAdapter() { 259 @Override 260 public void onAnimationEnd(Animation animation) { 261 commitDialpadFragmentHide(); 262 } 263 }; 264 /** Listener used to send search queries to the phone search fragment. */ 265 private final TextWatcher mPhoneSearchQueryTextListener = 266 new TextWatcher() { 267 @Override 268 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 269 270 @Override 271 public void onTextChanged(CharSequence s, int start, int before, int count) { 272 final String newText = s.toString(); 273 if (newText.equals(mSearchQuery)) { 274 // If the query hasn't changed (perhaps due to activity being destroyed 275 // and restored, or user launching the same DIAL intent twice), then there is 276 // no need to do anything here. 277 return; 278 } 279 280 if (count != 0) { 281 PerformanceReport.recordClick(UiAction.Type.TEXT_CHANGE_WITH_INPUT); 282 } 283 284 if (DEBUG) { 285 LogUtil.v("DialtactsActivity.onTextChanged", "called with new query: " + newText); 286 LogUtil.v("DialtactsActivity.onTextChanged", "previous query: " + mSearchQuery); 287 } 288 mSearchQuery = newText; 289 290 // TODO: show p13n when newText is empty. 291 // Show search fragment only when the query string is changed to non-empty text. 292 if (!TextUtils.isEmpty(newText)) { 293 // Call enterSearchUi only if we are switching search modes, or showing a search 294 // fragment for the first time. 295 final boolean sameSearchMode = 296 (mIsDialpadShown && mInDialpadSearch) || (!mIsDialpadShown && mInRegularSearch); 297 if (!sameSearchMode) { 298 enterSearchUi(mIsDialpadShown, mSearchQuery, true /* animate */); 299 } 300 } 301 302 if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { 303 mSmartDialSearchFragment.setQueryString(mSearchQuery); 304 } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { 305 mRegularSearchFragment.setQueryString(mSearchQuery); 306 } else if (mNewSearchFragment != null) { 307 mNewSearchFragment.setQuery(mSearchQuery); 308 } 309 } 310 311 @Override 312 public void afterTextChanged(Editable s) {} 313 }; 314 /** Open the search UI when the user clicks on the search box. */ 315 private final View.OnClickListener mSearchViewOnClickListener = 316 new View.OnClickListener() { 317 @Override 318 public void onClick(View v) { 319 if (!isInSearchUi()) { 320 PerformanceReport.recordClick(UiAction.Type.OPEN_SEARCH); 321 mActionBarController.onSearchBoxTapped(); 322 enterSearchUi( 323 false /* smartDialSearch */, mSearchView.getText().toString(), true /* animate */); 324 } 325 } 326 }; 327 328 private int mActionBarHeight; 329 private int mPreviouslySelectedTabIndex; 330 /** Handles the user closing the soft keyboard. */ 331 private final View.OnKeyListener mSearchEditTextLayoutListener = 332 new View.OnKeyListener() { 333 @Override 334 public boolean onKey(View v, int keyCode, KeyEvent event) { 335 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { 336 if (TextUtils.isEmpty(mSearchView.getText().toString())) { 337 // If the search term is empty, close the search UI. 338 PerformanceReport.recordClick(UiAction.Type.CLOSE_SEARCH_WITH_HIDE_BUTTON); 339 maybeExitSearchUi(); 340 } else { 341 // If the search term is not empty, show the dialpad fab. 342 if (!mFloatingActionButtonController.isVisible()) { 343 PerformanceReport.recordClick(UiAction.Type.HIDE_KEYBOARD_IN_SEARCH); 344 } 345 showFabInSearchUi(); 346 } 347 } 348 return false; 349 } 350 }; 351 /** 352 * The text returned from a voice search query. Set in {@link #onActivityResult} and used in 353 * {@link #onResume()} to populate the search box. 354 */ 355 private String mVoiceSearchQuery; 356 357 /** 358 * @param tab the TAB_INDEX_* constant in {@link ListsFragment} 359 * @return A intent that will open the DialtactsActivity into the specified tab. The intent for 360 * each tab will be unique. 361 */ 362 public static Intent getShowTabIntent(Context context, int tab) { 363 Intent intent = new Intent(context, DialtactsActivity.class); 364 intent.setAction(ACTION_SHOW_TAB); 365 intent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, tab); 366 intent.setData( 367 new Uri.Builder() 368 .scheme("intent") 369 .authority(context.getPackageName()) 370 .appendPath(TAG) 371 .appendQueryParameter(DialtactsActivity.EXTRA_SHOW_TAB, String.valueOf(tab)) 372 .build()); 373 374 return intent; 375 } 376 377 @Override 378 public boolean dispatchTouchEvent(MotionEvent ev) { 379 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 380 TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); 381 } 382 return super.dispatchTouchEvent(ev); 383 } 384 385 @Override 386 protected void onCreate(Bundle savedInstanceState) { 387 Trace.beginSection(TAG + " onCreate"); 388 super.onCreate(savedInstanceState); 389 390 mFirstLaunch = true; 391 isLastTabEnabled = ConfigProviderBindings.get(this).getBoolean("last_tab_enabled", false); 392 393 final Resources resources = getResources(); 394 mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height_large); 395 396 Trace.beginSection(TAG + " setContentView"); 397 setContentView(R.layout.dialtacts_activity); 398 Trace.endSection(); 399 getWindow().setBackgroundDrawable(null); 400 401 Trace.beginSection(TAG + " setup Views"); 402 final ActionBar actionBar = getActionBarSafely(); 403 actionBar.setCustomView(R.layout.search_edittext); 404 actionBar.setDisplayShowCustomEnabled(true); 405 actionBar.setBackgroundDrawable(null); 406 407 SearchEditTextLayout searchEditTextLayout = 408 (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container); 409 searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener); 410 411 mActionBarController = new ActionBarController(this, searchEditTextLayout); 412 413 mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view); 414 mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); 415 mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button); 416 searchEditTextLayout 417 .findViewById(R.id.search_box_collapsed) 418 .setOnClickListener(mSearchViewOnClickListener); 419 searchEditTextLayout.setCallback( 420 new SearchEditTextLayout.Callback() { 421 @Override 422 public void onBackButtonClicked() { 423 onBackPressed(); 424 } 425 426 @Override 427 public void onSearchViewClicked() { 428 // Hide FAB, as the keyboard is shown. 429 mFloatingActionButtonController.scaleOut(); 430 } 431 }); 432 433 mIsLandscape = 434 getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; 435 mPreviouslySelectedTabIndex = DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL; 436 FloatingActionButton floatingActionButton = 437 (FloatingActionButton) findViewById(R.id.floating_action_button); 438 floatingActionButton.setOnClickListener(this); 439 mFloatingActionButtonController = 440 new FloatingActionButtonController(this, floatingActionButton); 441 442 ImageButton optionsMenuButton = 443 (ImageButton) searchEditTextLayout.findViewById(R.id.dialtacts_options_menu_button); 444 optionsMenuButton.setOnClickListener(this); 445 mOverflowMenu = buildOptionsMenu(optionsMenuButton); 446 optionsMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener()); 447 448 // Add the favorites fragment but only if savedInstanceState is null. Otherwise the 449 // fragment manager is responsible for recreating it. 450 if (savedInstanceState == null) { 451 getFragmentManager() 452 .beginTransaction() 453 .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT) 454 .commit(); 455 } else { 456 mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY); 457 mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI); 458 mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI); 459 mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH); 460 mWasConfigurationChange = savedInstanceState.getBoolean(KEY_WAS_CONFIGURATION_CHANGE); 461 mShowDialpadOnResume = savedInstanceState.getBoolean(KEY_IS_DIALPAD_SHOWN); 462 mActionBarController.restoreInstanceState(savedInstanceState); 463 } 464 465 final boolean isLayoutRtl = ViewUtil.isRtl(); 466 if (mIsLandscape) { 467 mSlideIn = 468 AnimationUtils.loadAnimation( 469 this, isLayoutRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right); 470 mSlideOut = 471 AnimationUtils.loadAnimation( 472 this, isLayoutRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right); 473 } else { 474 mSlideIn = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom); 475 mSlideOut = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom); 476 } 477 478 mSlideIn.setInterpolator(AnimUtils.EASE_IN); 479 mSlideOut.setInterpolator(AnimUtils.EASE_OUT); 480 481 mSlideIn.setAnimationListener(mSlideInListener); 482 mSlideOut.setAnimationListener(mSlideOutListener); 483 484 mParentLayout = (CoordinatorLayout) findViewById(R.id.dialtacts_mainlayout); 485 mParentLayout.setOnDragListener(new LayoutOnDragListener()); 486 ViewUtil.doOnGlobalLayout( 487 floatingActionButton, 488 view -> { 489 int screenWidth = mParentLayout.getWidth(); 490 mFloatingActionButtonController.setScreenWidth(screenWidth); 491 mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); 492 }); 493 494 Trace.endSection(); 495 496 Trace.beginSection(TAG + " initialize smart dialing"); 497 mDialerDatabaseHelper = Database.get(this).getDatabaseHelper(this); 498 SmartDialPrefix.initializeNanpSettings(this); 499 Trace.endSection(); 500 501 mP13nLogger = P13nLogging.get(getApplicationContext()); 502 mP13nRanker = P13nRanking.get(getApplicationContext()); 503 Trace.endSection(); 504 } 505 506 @NonNull 507 private ActionBar getActionBarSafely() { 508 return Assert.isNotNull(getSupportActionBar()); 509 } 510 511 @Override 512 protected void onResume() { 513 LogUtil.d("DialtactsActivity.onResume", ""); 514 Trace.beginSection(TAG + " onResume"); 515 super.onResume(); 516 517 // Some calls may not be recorded (eg. from quick contact), 518 // so we should restart recording after these calls. (Recorded call is stopped) 519 PostCall.restartPerformanceRecordingIfARecentCallExist(this); 520 if (!PerformanceReport.isRecording()) { 521 PerformanceReport.startRecording(); 522 } 523 524 mStateSaved = false; 525 if (mFirstLaunch) { 526 displayFragment(getIntent()); 527 } else if (!phoneIsInUse() && mInCallDialpadUp) { 528 hideDialpadFragment(false, true); 529 mInCallDialpadUp = false; 530 } else if (mShowDialpadOnResume) { 531 showDialpadFragment(false); 532 mShowDialpadOnResume = false; 533 } else { 534 PostCall.promptUserForMessageIfNecessary(this, mParentLayout); 535 } 536 537 // If there was a voice query result returned in the {@link #onActivityResult} callback, it 538 // will have been stashed in mVoiceSearchQuery since the search results fragment cannot be 539 // shown until onResume has completed. Active the search UI and set the search term now. 540 if (!TextUtils.isEmpty(mVoiceSearchQuery)) { 541 mActionBarController.onSearchBoxTapped(); 542 mSearchView.setText(mVoiceSearchQuery); 543 mVoiceSearchQuery = null; 544 } 545 546 if (mIsRestarting) { 547 // This is only called when the activity goes from resumed -> paused -> resumed, so it 548 // will not cause an extra view to be sent out on rotation 549 if (mIsDialpadShown) { 550 Logger.get(this).logScreenView(ScreenEvent.Type.DIALPAD, this); 551 } 552 mIsRestarting = false; 553 } 554 555 prepareVoiceSearchButton(); 556 if (!mWasConfigurationChange) { 557 mDialerDatabaseHelper.startSmartDialUpdateThread(); 558 } 559 mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); 560 561 if (mFirstLaunch) { 562 // Only process the Intent the first time onResume() is called after receiving it 563 if (Calls.CONTENT_TYPE.equals(getIntent().getType())) { 564 // Externally specified extras take precedence to EXTRA_SHOW_TAB, which is only 565 // used internally. 566 final Bundle extras = getIntent().getExtras(); 567 if (extras != null && extras.getInt(Calls.EXTRA_CALL_TYPE_FILTER) == Calls.VOICEMAIL_TYPE) { 568 mListsFragment.showTab(DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL); 569 Logger.get(this).logImpression(DialerImpression.Type.VVM_NOTIFICATION_CLICKED); 570 } else { 571 mListsFragment.showTab(DialtactsPagerAdapter.TAB_INDEX_HISTORY); 572 } 573 } else if (getIntent().hasExtra(EXTRA_SHOW_TAB)) { 574 int index = 575 getIntent().getIntExtra(EXTRA_SHOW_TAB, DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL); 576 if (index < mListsFragment.getTabCount()) { 577 // Hide dialpad since this is an explicit intent to show a specific tab, which is coming 578 // from missed call or voicemail notification. 579 hideDialpadFragment(false, false); 580 exitSearchUi(); 581 mListsFragment.showTab(index); 582 } 583 } 584 585 if (getIntent().getBooleanExtra(EXTRA_CLEAR_NEW_VOICEMAILS, false)) { 586 LogUtil.i("DialtactsActivity.onResume", "clearing all new voicemails"); 587 CallLogNotificationsService.markAllNewVoicemailsAsOld(this); 588 } 589 } 590 591 mFirstLaunch = false; 592 593 setSearchBoxHint(); 594 timeTabSelected = SystemClock.elapsedRealtime(); 595 596 mP13nLogger.reset(); 597 mP13nRanker.refresh( 598 new P13nRefreshCompleteListener() { 599 @Override 600 public void onP13nRefreshComplete() { 601 // TODO: make zero-query search results visible 602 } 603 }); 604 Trace.endSection(); 605 } 606 607 @Override 608 protected void onRestart() { 609 super.onRestart(); 610 mIsRestarting = true; 611 } 612 613 @Override 614 protected void onPause() { 615 if (mClearSearchOnPause) { 616 hideDialpadAndSearchUi(); 617 mClearSearchOnPause = false; 618 } 619 if (mSlideOut.hasStarted() && !mSlideOut.hasEnded()) { 620 commitDialpadFragmentHide(); 621 } 622 super.onPause(); 623 } 624 625 @Override 626 protected void onStop() { 627 super.onStop(); 628 boolean timeoutElapsed = 629 SystemClock.elapsedRealtime() - timeTabSelected >= HISTORY_TAB_SEEN_TIMEOUT; 630 boolean isOnHistoryTab = 631 mListsFragment.getCurrentTabIndex() == DialtactsPagerAdapter.TAB_INDEX_HISTORY; 632 if (isOnHistoryTab 633 && timeoutElapsed 634 && !isChangingConfigurations() 635 && !getSystemService(KeyguardManager.class).isKeyguardLocked()) { 636 mListsFragment.markMissedCallsAsReadAndRemoveNotifications(); 637 } 638 DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(this) 639 .edit() 640 .putInt(KEY_LAST_TAB, mListsFragment.getCurrentTabIndex()) 641 .apply(); 642 } 643 644 @Override 645 protected void onSaveInstanceState(Bundle outState) { 646 super.onSaveInstanceState(outState); 647 outState.putString(KEY_SEARCH_QUERY, mSearchQuery); 648 outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch); 649 outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch); 650 outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch); 651 outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown); 652 outState.putBoolean(KEY_WAS_CONFIGURATION_CHANGE, isChangingConfigurations()); 653 mActionBarController.saveInstanceState(outState); 654 mStateSaved = true; 655 } 656 657 @Override 658 public void onAttachFragment(final Fragment fragment) { 659 LogUtil.d("DialtactsActivity.onAttachFragment", "fragment: %s", fragment); 660 if (fragment instanceof DialpadFragment) { 661 mDialpadFragment = (DialpadFragment) fragment; 662 if (!mIsDialpadShown && !mShowDialpadOnResume) { 663 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 664 transaction.hide(mDialpadFragment); 665 transaction.commit(); 666 } 667 } else if (fragment instanceof SmartDialSearchFragment) { 668 mSmartDialSearchFragment = (SmartDialSearchFragment) fragment; 669 mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this); 670 if (!TextUtils.isEmpty(mDialpadQuery)) { 671 mSmartDialSearchFragment.setAddToContactNumber(mDialpadQuery); 672 } 673 } else if (fragment instanceof SearchFragment) { 674 mRegularSearchFragment = (RegularSearchFragment) fragment; 675 mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this); 676 } else if (fragment instanceof ListsFragment) { 677 mListsFragment = (ListsFragment) fragment; 678 mListsFragment.addOnPageChangeListener(this); 679 } else if (fragment instanceof NewSearchFragment) { 680 mNewSearchFragment = (NewSearchFragment) fragment; 681 } 682 if (fragment instanceof SearchFragment) { 683 final SearchFragment searchFragment = (SearchFragment) fragment; 684 searchFragment.setReranker( 685 new CursorReranker() { 686 @Override 687 @MainThread 688 public Cursor rerankCursor(Cursor data) { 689 Assert.isMainThread(); 690 String queryString = searchFragment.getQueryString(); 691 return mP13nRanker.rankCursor(data, queryString == null ? 0 : queryString.length()); 692 } 693 }); 694 searchFragment.addOnLoadFinishedListener( 695 new OnLoadFinishedListener() { 696 @Override 697 public void onLoadFinished() { 698 mP13nLogger.onSearchQuery( 699 searchFragment.getQueryString(), 700 (PhoneNumberListAdapter) searchFragment.getAdapter()); 701 } 702 }); 703 } 704 } 705 706 protected void handleMenuSettings() { 707 final Intent intent = new Intent(this, DialerSettingsActivity.class); 708 startActivity(intent); 709 } 710 711 @Override 712 public void onClick(View view) { 713 int resId = view.getId(); 714 if (resId == R.id.floating_action_button) { 715 if (!mIsDialpadShown) { 716 PerformanceReport.recordClick(UiAction.Type.OPEN_DIALPAD); 717 mInCallDialpadUp = false; 718 showDialpadFragment(true); 719 PostCall.closePrompt(); 720 } 721 } else if (resId == R.id.voice_search_button) { 722 try { 723 startActivityForResult( 724 new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 725 ACTIVITY_REQUEST_CODE_VOICE_SEARCH); 726 } catch (ActivityNotFoundException e) { 727 Toast.makeText( 728 DialtactsActivity.this, R.string.voice_search_not_available, Toast.LENGTH_SHORT) 729 .show(); 730 } 731 } else if (resId == R.id.dialtacts_options_menu_button) { 732 mOverflowMenu.show(); 733 } else { 734 Assert.fail("Unexpected onClick event from " + view); 735 } 736 } 737 738 @Override 739 public boolean onMenuItemClick(MenuItem item) { 740 if (!isSafeToCommitTransactions()) { 741 return true; 742 } 743 744 int resId = item.getItemId(); 745 if (resId == R.id.menu_history) { 746 PerformanceReport.recordClick(UiAction.Type.OPEN_CALL_HISTORY); 747 final Intent intent = new Intent(this, CallLogActivity.class); 748 startActivity(intent); 749 } else if (resId == R.id.menu_clear_frequents) { 750 ClearFrequentsDialog.show(getFragmentManager()); 751 Logger.get(this).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, this); 752 return true; 753 } else if (resId == R.id.menu_call_settings) { 754 handleMenuSettings(); 755 Logger.get(this).logScreenView(ScreenEvent.Type.SETTINGS, this); 756 return true; 757 } else if (resId == R.id.menu_new_ui_launcher_shortcut) { 758 MainComponent.get(this).getMain().createNewUiLauncherShortcut(this); 759 return true; 760 } 761 return false; 762 } 763 764 @Override 765 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 766 LogUtil.i( 767 "DialtactsActivity.onActivityResult", 768 "requestCode:%d, resultCode:%d", 769 requestCode, 770 resultCode); 771 if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) { 772 if (resultCode == RESULT_OK) { 773 final ArrayList<String> matches = 774 data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); 775 if (matches.size() > 0) { 776 mVoiceSearchQuery = matches.get(0); 777 } else { 778 LogUtil.i("DialtactsActivity.onActivityResult", "voice search - nothing heard"); 779 } 780 } else { 781 LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed"); 782 } 783 } else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_COMPOSE) { 784 if (resultCode == RESULT_FIRST_USER) { 785 LogUtil.i( 786 "DialtactsActivity.onActivityResult", "returned from call composer, error occurred"); 787 String message = 788 getString( 789 R.string.call_composer_connection_failed, 790 data.getStringExtra(CallComposerActivity.KEY_CONTACT_NAME)); 791 Snackbar.make(mParentLayout, message, Snackbar.LENGTH_LONG).show(); 792 } else { 793 LogUtil.i("DialtactsActivity.onActivityResult", "returned from call composer, no error"); 794 } 795 } else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_DETAILS) { 796 if (resultCode == RESULT_OK 797 && data != null 798 && data.getBooleanExtra(CallDetailsActivity.EXTRA_HAS_ENRICHED_CALL_DATA, false)) { 799 String number = data.getStringExtra(CallDetailsActivity.EXTRA_PHONE_NUMBER); 800 int snackbarDurationMillis = 5_000; 801 Snackbar.make(mParentLayout, getString(R.string.ec_data_deleted), snackbarDurationMillis) 802 .setAction( 803 R.string.view_conversation, 804 v -> startActivity(IntentProvider.getSendSmsIntentProvider(number).getIntent(this))) 805 .setActionTextColor(getResources().getColor(R.color.dialer_snackbar_action_text_color)) 806 .show(); 807 } 808 } 809 super.onActivityResult(requestCode, resultCode, data); 810 } 811 812 /** 813 * Update the number of unread voicemails (potentially other tabs) displayed next to the tab icon. 814 */ 815 public void updateTabUnreadCounts() { 816 mListsFragment.updateTabUnreadCounts(); 817 } 818 819 /** 820 * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual 821 * updates are handled by a callback which is invoked after the dialpad fragment is shown. 822 * 823 * @see #onDialpadShown 824 */ 825 private void showDialpadFragment(boolean animate) { 826 LogUtil.d("DialtactActivity.showDialpadFragment", "animate: %b", animate); 827 if (mIsDialpadShown || mStateSaved) { 828 return; 829 } 830 mIsDialpadShown = true; 831 832 mListsFragment.setUserVisibleHint(false); 833 834 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 835 if (mDialpadFragment == null) { 836 mDialpadFragment = new DialpadFragment(); 837 ft.add(R.id.dialtacts_container, mDialpadFragment, TAG_DIALPAD_FRAGMENT); 838 } else { 839 ft.show(mDialpadFragment); 840 } 841 842 mDialpadFragment.setAnimate(animate); 843 Logger.get(this).logScreenView(ScreenEvent.Type.DIALPAD, this); 844 ft.commit(); 845 846 if (animate) { 847 mFloatingActionButtonController.scaleOut(); 848 maybeEnterSearchUi(); 849 } else { 850 mFloatingActionButtonController.setVisible(false); 851 maybeEnterSearchUi(); 852 } 853 mActionBarController.onDialpadUp(); 854 855 Assert.isNotNull(mListsFragment.getView()).animate().alpha(0).withLayer(); 856 857 //adjust the title, so the user will know where we're at when the activity start/resumes. 858 setTitle(R.string.launcherDialpadActivityLabel); 859 } 860 861 /** Callback from child DialpadFragment when the dialpad is shown. */ 862 public void onDialpadShown() { 863 LogUtil.d("DialtactsActivity.onDialpadShown", ""); 864 Assert.isNotNull(mDialpadFragment); 865 if (mDialpadFragment.getAnimate()) { 866 Assert.isNotNull(mDialpadFragment.getView()).startAnimation(mSlideIn); 867 } else { 868 mDialpadFragment.setYFraction(0); 869 } 870 871 updateSearchFragmentPosition(); 872 } 873 874 /** 875 * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in a 876 * callback after the hide animation ends. 877 * 878 * @see #commitDialpadFragmentHide 879 */ 880 public void hideDialpadFragment(boolean animate, boolean clearDialpad) { 881 if (mDialpadFragment == null || mDialpadFragment.getView() == null) { 882 return; 883 } 884 if (clearDialpad) { 885 // Temporarily disable accessibility when we clear the dialpad, since it should be 886 // invisible and should not announce anything. 887 mDialpadFragment 888 .getDigitsWidget() 889 .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 890 mDialpadFragment.clearDialpad(); 891 mDialpadFragment 892 .getDigitsWidget() 893 .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); 894 } 895 if (!mIsDialpadShown) { 896 return; 897 } 898 mIsDialpadShown = false; 899 mDialpadFragment.setAnimate(animate); 900 mListsFragment.setUserVisibleHint(true); 901 mListsFragment.sendScreenViewForCurrentPosition(); 902 903 updateSearchFragmentPosition(); 904 905 mFloatingActionButtonController.align(getFabAlignment(), animate); 906 if (animate) { 907 mDialpadFragment.getView().startAnimation(mSlideOut); 908 } else { 909 commitDialpadFragmentHide(); 910 } 911 912 mActionBarController.onDialpadDown(); 913 914 if (isInSearchUi()) { 915 if (TextUtils.isEmpty(mSearchQuery)) { 916 exitSearchUi(); 917 } 918 } 919 //reset the title to normal. 920 setTitle(R.string.launcherActivityLabel); 921 } 922 923 /** Finishes hiding the dialpad fragment after any animations are completed. */ 924 private void commitDialpadFragmentHide() { 925 if (!mStateSaved 926 && mDialpadFragment != null 927 && !mDialpadFragment.isHidden() 928 && !isDestroyed()) { 929 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 930 ft.hide(mDialpadFragment); 931 ft.commit(); 932 } 933 mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); 934 } 935 936 private void updateSearchFragmentPosition() { 937 SearchFragment fragment = null; 938 if (mSmartDialSearchFragment != null) { 939 fragment = mSmartDialSearchFragment; 940 } else if (mRegularSearchFragment != null) { 941 fragment = mRegularSearchFragment; 942 } 943 LogUtil.d( 944 "DialtactsActivity.updateSearchFragmentPosition", 945 "fragment: %s, isVisible: %b", 946 fragment, 947 fragment != null && fragment.isVisible()); 948 if (fragment != null) { 949 // We need to force animation here even when fragment is not visible since it might not be 950 // visible immediately after screen orientation change and dialpad height would not be 951 // available immediately which is required to update position. By forcing an animation, 952 // position will be updated after a delay by when the dialpad height would be available. 953 fragment.updatePosition(true /* animate */); 954 } 955 } 956 957 @Override 958 public boolean isInSearchUi() { 959 return mInDialpadSearch || mInRegularSearch; 960 } 961 962 @Override 963 public boolean hasSearchQuery() { 964 return !TextUtils.isEmpty(mSearchQuery); 965 } 966 967 private void setNotInSearchUi() { 968 mInDialpadSearch = false; 969 mInRegularSearch = false; 970 } 971 972 private void hideDialpadAndSearchUi() { 973 if (mIsDialpadShown) { 974 hideDialpadFragment(false, true); 975 } else { 976 exitSearchUi(); 977 } 978 } 979 980 private void prepareVoiceSearchButton() { 981 final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 982 if (canIntentBeHandled(voiceIntent)) { 983 mVoiceSearchButton.setVisibility(View.VISIBLE); 984 mVoiceSearchButton.setOnClickListener(this); 985 } else { 986 mVoiceSearchButton.setVisibility(View.GONE); 987 } 988 } 989 990 public boolean isNearbyPlacesSearchEnabled() { 991 return false; 992 } 993 994 protected int getSearchBoxHint() { 995 return R.string.dialer_hint_find_contact; 996 } 997 998 /** Sets the hint text for the contacts search box */ 999 private void setSearchBoxHint() { 1000 SearchEditTextLayout searchEditTextLayout = 1001 (SearchEditTextLayout) 1002 getActionBarSafely().getCustomView().findViewById(R.id.search_view_container); 1003 ((TextView) searchEditTextLayout.findViewById(R.id.search_box_start_search)) 1004 .setHint(getSearchBoxHint()); 1005 } 1006 1007 protected OptionsPopupMenu buildOptionsMenu(View invoker) { 1008 final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker); 1009 popupMenu.inflate(R.menu.dialtacts_options); 1010 popupMenu.setOnMenuItemClickListener(this); 1011 return popupMenu; 1012 } 1013 1014 @Override 1015 public boolean onCreateOptionsMenu(Menu menu) { 1016 if (mPendingSearchViewQuery != null) { 1017 mSearchView.setText(mPendingSearchViewQuery); 1018 mPendingSearchViewQuery = null; 1019 } 1020 if (mActionBarController != null) { 1021 mActionBarController.restoreActionBarOffset(); 1022 } 1023 return false; 1024 } 1025 1026 /** 1027 * Returns true if the intent is due to hitting the green send key (hardware call button: 1028 * KEYCODE_CALL) while in a call. 1029 * 1030 * @param intent the intent that launched this activity 1031 * @return true if the intent is due to hitting the green send key while in a call 1032 */ 1033 private boolean isSendKeyWhileInCall(Intent intent) { 1034 // If there is a call in progress and the user launched the dialer by hitting the call 1035 // button, go straight to the in-call screen. 1036 final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction()); 1037 1038 // When KEYCODE_CALL event is handled it dispatches an intent with the ACTION_CALL_BUTTON. 1039 // Besides of checking the intent action, we must check if the phone is really during a 1040 // call in order to decide whether to ignore the event or continue to display the activity. 1041 if (callKey && phoneIsInUse()) { 1042 TelecomUtil.showInCallScreen(this, false); 1043 return true; 1044 } 1045 1046 return false; 1047 } 1048 1049 /** 1050 * Sets the current tab based on the intent's request type 1051 * 1052 * @param intent Intent that contains information about which tab should be selected 1053 */ 1054 private void displayFragment(Intent intent) { 1055 // If we got here by hitting send and we're in call forward along to the in-call activity 1056 if (isSendKeyWhileInCall(intent)) { 1057 finish(); 1058 return; 1059 } 1060 1061 final boolean showDialpadChooser = 1062 !ACTION_SHOW_TAB.equals(intent.getAction()) 1063 && phoneIsInUse() 1064 && !DialpadFragment.isAddCallMode(intent); 1065 if (showDialpadChooser || (intent.getData() != null && isDialIntent(intent))) { 1066 showDialpadFragment(false); 1067 mDialpadFragment.setStartedFromNewIntent(true); 1068 if (showDialpadChooser && !mDialpadFragment.isVisible()) { 1069 mInCallDialpadUp = true; 1070 } 1071 } else if (isLastTabEnabled) { 1072 @TabIndex 1073 int tabIndex = 1074 DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(this) 1075 .getInt(KEY_LAST_TAB, DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL); 1076 // If voicemail tab is saved and its availability changes, we still move to the voicemail tab 1077 // but it is quickly removed and shown the contacts tab. 1078 if (mListsFragment != null) { 1079 mListsFragment.showTab(tabIndex); 1080 PerformanceReport.setStartingTabIndex(tabIndex); 1081 } else { 1082 PerformanceReport.setStartingTabIndex(DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL); 1083 } 1084 } 1085 } 1086 1087 @Override 1088 public void onNewIntent(Intent newIntent) { 1089 setIntent(newIntent); 1090 mFirstLaunch = true; 1091 1092 mStateSaved = false; 1093 displayFragment(newIntent); 1094 1095 invalidateOptionsMenu(); 1096 } 1097 1098 /** Returns true if the given intent contains a phone number to populate the dialer with */ 1099 private boolean isDialIntent(Intent intent) { 1100 final String action = intent.getAction(); 1101 if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) { 1102 return true; 1103 } 1104 if (Intent.ACTION_VIEW.equals(action)) { 1105 final Uri data = intent.getData(); 1106 if (data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme())) { 1107 return true; 1108 } 1109 } 1110 return false; 1111 } 1112 1113 /** Shows the search fragment */ 1114 private void enterSearchUi(boolean smartDialSearch, String query, boolean animate) { 1115 if (mStateSaved || getFragmentManager().isDestroyed()) { 1116 // Weird race condition where fragment is doing work after the activity is destroyed 1117 // due to talkback being on (b/10209937). Just return since we can't do any 1118 // constructive here. 1119 return; 1120 } 1121 1122 if (DEBUG) { 1123 LogUtil.v("DialtactsActivity.enterSearchUi", "smart dial " + smartDialSearch); 1124 } 1125 1126 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 1127 if (mInDialpadSearch && mSmartDialSearchFragment != null) { 1128 transaction.remove(mSmartDialSearchFragment); 1129 } else if (mInRegularSearch && mRegularSearchFragment != null) { 1130 transaction.remove(mRegularSearchFragment); 1131 } 1132 1133 final String tag; 1134 boolean useNewSearch = 1135 ConfigProviderBindings.get(this).getBoolean("enable_new_search_fragment", false); 1136 if (useNewSearch) { 1137 tag = TAG_NEW_SEARCH_FRAGMENT; 1138 } else if (smartDialSearch) { 1139 tag = TAG_SMARTDIAL_SEARCH_FRAGMENT; 1140 } else { 1141 tag = TAG_REGULAR_SEARCH_FRAGMENT; 1142 } 1143 mInDialpadSearch = smartDialSearch; 1144 mInRegularSearch = !smartDialSearch; 1145 1146 mFloatingActionButtonController.scaleOut(); 1147 1148 if (animate) { 1149 transaction.setCustomAnimations(android.R.animator.fade_in, 0); 1150 } else { 1151 transaction.setTransition(FragmentTransaction.TRANSIT_NONE); 1152 } 1153 1154 Fragment fragment = getFragmentManager().findFragmentByTag(tag); 1155 if (fragment == null) { 1156 if (useNewSearch) { 1157 fragment = new NewSearchFragment(); 1158 } else if (smartDialSearch) { 1159 fragment = new SmartDialSearchFragment(); 1160 } else { 1161 fragment = Bindings.getLegacy(this).newRegularSearchFragment(); 1162 ((SearchFragment) fragment) 1163 .setOnTouchListener( 1164 (v, event) -> { 1165 // Show the FAB when the user touches the lists fragment and the soft 1166 // keyboard is hidden. 1167 hideDialpadFragment(true, false); 1168 v.performClick(); 1169 return false; 1170 }); 1171 } 1172 transaction.add(R.id.dialtacts_frame, fragment, tag); 1173 } else { 1174 // TODO: if this is a transition from dialpad to searchbar, animate fragment 1175 // down, and vice versa. Perhaps just add a coordinator behavior with the search bar. 1176 transaction.show(fragment); 1177 } 1178 1179 // DialtactsActivity will provide the options menu 1180 fragment.setHasOptionsMenu(false); 1181 1182 // Will show empty list if P13nRanker is not enabled. Else, re-ranked list by the ranker. 1183 if (!useNewSearch) { 1184 ((SearchFragment) fragment) 1185 .setShowEmptyListForNullQuery(mP13nRanker.shouldShowEmptyListForNullQuery()); 1186 } else { 1187 // TODO: add p13n ranker to new search. 1188 } 1189 1190 if (!smartDialSearch && !useNewSearch) { 1191 ((SearchFragment) fragment).setQueryString(query); 1192 } else if (useNewSearch) { 1193 ((NewSearchFragment) fragment).setQuery(query); 1194 } 1195 transaction.commit(); 1196 1197 if (animate) { 1198 Assert.isNotNull(mListsFragment.getView()).animate().alpha(0).withLayer(); 1199 } 1200 mListsFragment.setUserVisibleHint(false); 1201 1202 if (smartDialSearch) { 1203 Logger.get(this).logScreenView(ScreenEvent.Type.SMART_DIAL_SEARCH, this); 1204 } else { 1205 Logger.get(this).logScreenView(ScreenEvent.Type.REGULAR_SEARCH, this); 1206 } 1207 } 1208 1209 /** Hides the search fragment */ 1210 private void exitSearchUi() { 1211 // See related bug in enterSearchUI(); 1212 if (getFragmentManager().isDestroyed() || mStateSaved) { 1213 return; 1214 } 1215 1216 mSearchView.setText(null); 1217 1218 if (mDialpadFragment != null) { 1219 mDialpadFragment.clearDialpad(); 1220 } 1221 1222 setNotInSearchUi(); 1223 1224 // Restore the FAB for the lists fragment. 1225 if (getFabAlignment() != FloatingActionButtonController.ALIGN_END) { 1226 mFloatingActionButtonController.setVisible(false); 1227 } 1228 mFloatingActionButtonController.scaleIn(FAB_SCALE_IN_DELAY_MS); 1229 onPageScrolled(mListsFragment.getCurrentTabIndex(), 0 /* offset */, 0 /* pixelOffset */); 1230 onPageSelected(mListsFragment.getCurrentTabIndex()); 1231 1232 final FragmentTransaction transaction = getFragmentManager().beginTransaction(); 1233 if (mSmartDialSearchFragment != null) { 1234 transaction.remove(mSmartDialSearchFragment); 1235 } 1236 if (mRegularSearchFragment != null) { 1237 transaction.remove(mRegularSearchFragment); 1238 } 1239 if (mNewSearchFragment != null) { 1240 transaction.remove(mNewSearchFragment); 1241 } 1242 transaction.commit(); 1243 1244 Assert.isNotNull(mListsFragment.getView()).animate().alpha(1).withLayer(); 1245 1246 if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { 1247 // If the dialpad fragment wasn't previously visible, then send a screen view because 1248 // we are exiting regular search. Otherwise, the screen view will be sent by 1249 // {@link #hideDialpadFragment}. 1250 mListsFragment.sendScreenViewForCurrentPosition(); 1251 mListsFragment.setUserVisibleHint(true); 1252 } 1253 1254 mActionBarController.onSearchUiExited(); 1255 } 1256 1257 @Override 1258 public void onBackPressed() { 1259 PerformanceReport.recordClick(UiAction.Type.PRESS_ANDROID_BACK_BUTTON); 1260 1261 if (mStateSaved) { 1262 return; 1263 } 1264 if (mIsDialpadShown) { 1265 if (TextUtils.isEmpty(mSearchQuery) 1266 || (mSmartDialSearchFragment != null 1267 && mSmartDialSearchFragment.isVisible() 1268 && mSmartDialSearchFragment.getAdapter().getCount() == 0)) { 1269 exitSearchUi(); 1270 } 1271 hideDialpadFragment(true, false); 1272 } else if (isInSearchUi()) { 1273 exitSearchUi(); 1274 DialerUtils.hideInputMethod(mParentLayout); 1275 } else { 1276 super.onBackPressed(); 1277 } 1278 } 1279 1280 private void maybeEnterSearchUi() { 1281 if (!isInSearchUi()) { 1282 enterSearchUi(true /* isSmartDial */, mSearchQuery, false); 1283 } 1284 } 1285 1286 /** @return True if the search UI was exited, false otherwise */ 1287 private boolean maybeExitSearchUi() { 1288 if (isInSearchUi() && TextUtils.isEmpty(mSearchQuery)) { 1289 exitSearchUi(); 1290 DialerUtils.hideInputMethod(mParentLayout); 1291 return true; 1292 } 1293 return false; 1294 } 1295 1296 private void showFabInSearchUi() { 1297 mFloatingActionButtonController.changeIcon( 1298 getResources().getDrawable(R.drawable.quantum_ic_dialpad_white_24, null), 1299 getResources().getString(R.string.action_menu_dialpad_button)); 1300 mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); 1301 mFloatingActionButtonController.scaleIn(FAB_SCALE_IN_DELAY_MS); 1302 } 1303 1304 @Override 1305 public void onDialpadQueryChanged(String query) { 1306 mDialpadQuery = query; 1307 if (mSmartDialSearchFragment != null) { 1308 mSmartDialSearchFragment.setAddToContactNumber(query); 1309 } 1310 final String normalizedQuery = 1311 SmartDialNameMatcher.normalizeNumber(query, SmartDialNameMatcher.LATIN_SMART_DIAL_MAP); 1312 1313 if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) { 1314 if (DEBUG) { 1315 LogUtil.v("DialtactsActivity.onDialpadQueryChanged", "new query: " + query); 1316 } 1317 if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { 1318 // This callback can happen if the dialpad fragment is recreated because of 1319 // activity destruction. In that case, don't update the search view because 1320 // that would bring the user back to the search fragment regardless of the 1321 // previous state of the application. Instead, just return here and let the 1322 // fragment manager correctly figure out whatever fragment was last displayed. 1323 if (!TextUtils.isEmpty(normalizedQuery)) { 1324 mPendingSearchViewQuery = normalizedQuery; 1325 } 1326 return; 1327 } 1328 mSearchView.setText(normalizedQuery); 1329 } 1330 1331 try { 1332 if (mDialpadFragment != null && mDialpadFragment.isVisible()) { 1333 mDialpadFragment.process_quote_emergency_unquote(normalizedQuery); 1334 } 1335 } catch (Exception ignored) { 1336 // Skip any exceptions for this piece of code 1337 } 1338 } 1339 1340 @Override 1341 public boolean onDialpadSpacerTouchWithEmptyQuery() { 1342 if (mInDialpadSearch 1343 && mSmartDialSearchFragment != null 1344 && !mSmartDialSearchFragment.isShowingPermissionRequest()) { 1345 PerformanceReport.recordClick(UiAction.Type.CLOSE_DIALPAD); 1346 hideDialpadFragment(true /* animate */, true /* clearDialpad */); 1347 return true; 1348 } 1349 return false; 1350 } 1351 1352 @Override 1353 public void onListFragmentScrollStateChange(int scrollState) { 1354 PerformanceReport.recordScrollStateChange(scrollState); 1355 if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { 1356 hideDialpadFragment(true, false); 1357 DialerUtils.hideInputMethod(mParentLayout); 1358 } 1359 } 1360 1361 @Override 1362 public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) { 1363 // TODO: No-op for now. This should eventually show/hide the actionBar based on 1364 // interactions with the ListsFragments. 1365 } 1366 1367 private boolean phoneIsInUse() { 1368 return TelecomUtil.isInCall(this); 1369 } 1370 1371 private boolean canIntentBeHandled(Intent intent) { 1372 final PackageManager packageManager = getPackageManager(); 1373 final List<ResolveInfo> resolveInfo = 1374 packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 1375 return resolveInfo != null && resolveInfo.size() > 0; 1376 } 1377 1378 /** Called when the user has long-pressed a contact tile to start a drag operation. */ 1379 @Override 1380 public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { 1381 mListsFragment.showRemoveView(true); 1382 } 1383 1384 @Override 1385 public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {} 1386 1387 /** Called when the user has released a contact tile after long-pressing it. */ 1388 @Override 1389 public void onDragFinished(int x, int y) { 1390 mListsFragment.showRemoveView(false); 1391 } 1392 1393 @Override 1394 public void onDroppedOnRemove() {} 1395 1396 /** 1397 * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer once it has 1398 * been attached to the activity. 1399 */ 1400 @Override 1401 public void setDragDropController(DragDropController dragController) { 1402 mDragDropController = dragController; 1403 mListsFragment.getRemoveView().setDragDropController(dragController); 1404 } 1405 1406 /** Implemented to satisfy {@link OldSpeedDialFragment.HostInterface} */ 1407 @Override 1408 public void showAllContactsTab() { 1409 if (mListsFragment != null) { 1410 mListsFragment.showTab(DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS); 1411 } 1412 } 1413 1414 /** Implemented to satisfy {@link CallLogFragment.HostInterface} */ 1415 @Override 1416 public void showDialpad() { 1417 showDialpadFragment(true); 1418 } 1419 1420 @Override 1421 public void enableFloatingButton(boolean enabled) { 1422 LogUtil.d("DialtactsActivity.enableFloatingButton", "enable: %b", enabled); 1423 // Floating button shouldn't be enabled when dialpad is shown. 1424 if (!isDialpadShown() || !enabled) { 1425 mFloatingActionButtonController.setVisible(enabled); 1426 } 1427 } 1428 1429 @Override 1430 public void onPickDataUri( 1431 Uri dataUri, boolean isVideoCall, CallSpecificAppData callSpecificAppData) { 1432 mClearSearchOnPause = true; 1433 PhoneNumberInteraction.startInteractionForPhoneCall( 1434 DialtactsActivity.this, dataUri, isVideoCall, callSpecificAppData); 1435 } 1436 1437 @Override 1438 public void onPickPhoneNumber( 1439 String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData) { 1440 if (phoneNumber == null) { 1441 // Invalid phone number, but let the call go through so that InCallUI can show 1442 // an error message. 1443 phoneNumber = ""; 1444 } 1445 1446 Intent intent = 1447 new CallIntentBuilder(phoneNumber, callSpecificAppData).setIsVideoCall(isVideoCall).build(); 1448 1449 DialerUtils.startActivityWithErrorToast(this, intent); 1450 mClearSearchOnPause = true; 1451 } 1452 1453 @Override 1454 public void onHomeInActionBarSelected() { 1455 exitSearchUi(); 1456 } 1457 1458 @Override 1459 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 1460 int tabIndex = mListsFragment.getCurrentTabIndex(); 1461 1462 // Scroll the button from center to end when moving from the Speed Dial to Call History tab. 1463 // In RTL, scroll when the current tab is Call History instead, since the order of the tabs 1464 // is reversed and the ViewPager returns the left tab position during scroll. 1465 boolean isRtl = ViewUtil.isRtl(); 1466 if (!isRtl && tabIndex == DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL && !mIsLandscape) { 1467 mFloatingActionButtonController.onPageScrolled(positionOffset); 1468 } else if (isRtl && tabIndex == DialtactsPagerAdapter.TAB_INDEX_HISTORY && !mIsLandscape) { 1469 mFloatingActionButtonController.onPageScrolled(1 - positionOffset); 1470 } else if (tabIndex != DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL) { 1471 mFloatingActionButtonController.onPageScrolled(1); 1472 } 1473 } 1474 1475 @Override 1476 public void onPageSelected(int position) { 1477 updateMissedCalls(); 1478 int tabIndex = mListsFragment.getCurrentTabIndex(); 1479 mPreviouslySelectedTabIndex = tabIndex; 1480 mFloatingActionButtonController.setVisible(true); 1481 timeTabSelected = SystemClock.elapsedRealtime(); 1482 } 1483 1484 @Override 1485 public void onPageScrollStateChanged(int state) {} 1486 1487 @Override 1488 public boolean isActionBarShowing() { 1489 return mActionBarController.isActionBarShowing(); 1490 } 1491 1492 @Override 1493 public boolean isDialpadShown() { 1494 return mIsDialpadShown; 1495 } 1496 1497 @Override 1498 public int getDialpadHeight() { 1499 if (mDialpadFragment != null) { 1500 return mDialpadFragment.getDialpadHeight(); 1501 } 1502 return 0; 1503 } 1504 1505 @Override 1506 public void setActionBarHideOffset(int offset) { 1507 getActionBarSafely().setHideOffset(offset); 1508 } 1509 1510 @Override 1511 public int getActionBarHeight() { 1512 return mActionBarHeight; 1513 } 1514 1515 private int getFabAlignment() { 1516 if (!mIsLandscape 1517 && !isInSearchUi() 1518 && mListsFragment.getCurrentTabIndex() == DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL) { 1519 return FloatingActionButtonController.ALIGN_MIDDLE; 1520 } 1521 return FloatingActionButtonController.ALIGN_END; 1522 } 1523 1524 private void updateMissedCalls() { 1525 if (mPreviouslySelectedTabIndex == DialtactsPagerAdapter.TAB_INDEX_HISTORY) { 1526 mListsFragment.markMissedCallsAsReadAndRemoveNotifications(); 1527 } 1528 } 1529 1530 @Override 1531 public void onDisambigDialogDismissed() { 1532 // Don't do anything; the app will remain open with favorites tiles displayed. 1533 } 1534 1535 @Override 1536 public void interactionError(@InteractionErrorCode int interactionErrorCode) { 1537 switch (interactionErrorCode) { 1538 case InteractionErrorCode.USER_LEAVING_ACTIVITY: 1539 // This is expected to happen if the user exits the activity before the interaction occurs. 1540 return; 1541 case InteractionErrorCode.CONTACT_NOT_FOUND: 1542 case InteractionErrorCode.CONTACT_HAS_NO_NUMBER: 1543 case InteractionErrorCode.OTHER_ERROR: 1544 default: 1545 // All other error codes are unexpected. For example, it should be impossible to start an 1546 // interaction with an invalid contact from the Dialtacts activity. 1547 Assert.fail("PhoneNumberInteraction error: " + interactionErrorCode); 1548 } 1549 } 1550 1551 @Override 1552 public void onRequestPermissionsResult( 1553 int requestCode, String[] permissions, int[] grantResults) { 1554 // This should never happen; it should be impossible to start an interaction without the 1555 // contacts permission from the Dialtacts activity. 1556 Assert.fail( 1557 String.format( 1558 Locale.US, 1559 "Permissions requested unexpectedly: %d/%s/%s", 1560 requestCode, 1561 Arrays.toString(permissions), 1562 Arrays.toString(grantResults))); 1563 } 1564 1565 @Override 1566 public void onActionModeStateChanged(boolean isEnabled) { 1567 isMultiSelectModeEnabled = isEnabled; 1568 } 1569 1570 @Override 1571 public boolean isActionModeStateEnabled() { 1572 return isMultiSelectModeEnabled; 1573 } 1574 1575 /** Popup menu accessible from the search bar */ 1576 protected class OptionsPopupMenu extends PopupMenu { 1577 1578 public OptionsPopupMenu(Context context, View anchor) { 1579 super(context, anchor, Gravity.END); 1580 } 1581 1582 @Override 1583 public void show() { 1584 Menu menu = getMenu(); 1585 MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents); 1586 clearFrequents.setVisible( 1587 PermissionsUtil.hasContactsReadPermissions(DialtactsActivity.this) 1588 && mListsFragment != null 1589 && mListsFragment.hasFrequents()); 1590 1591 menu.findItem(R.id.menu_history) 1592 .setVisible(PermissionsUtil.hasPhonePermissions(DialtactsActivity.this)); 1593 1594 Context context = DialtactsActivity.this.getApplicationContext(); 1595 MenuItem simulatorMenuItem = menu.findItem(R.id.menu_simulator_submenu); 1596 Simulator simulator = SimulatorComponent.get(context).getSimulator(); 1597 if (simulator.shouldShow()) { 1598 simulatorMenuItem.setVisible(true); 1599 simulatorMenuItem.setActionProvider(simulator.getActionProvider(context)); 1600 } else { 1601 simulatorMenuItem.setVisible(false); 1602 } 1603 1604 Main dialtacts = MainComponent.get(context).getMain(); 1605 menu.findItem(R.id.menu_new_ui_launcher_shortcut) 1606 .setVisible(dialtacts.isNewUiEnabled(context)); 1607 1608 super.show(); 1609 } 1610 } 1611 1612 /** 1613 * Listener that listens to drag events and sends their x and y coordinates to a {@link 1614 * DragDropController}. 1615 */ 1616 private class LayoutOnDragListener implements OnDragListener { 1617 1618 @Override 1619 public boolean onDrag(View v, DragEvent event) { 1620 if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) { 1621 mDragDropController.handleDragHovered(v, (int) event.getX(), (int) event.getY()); 1622 } 1623 return true; 1624 } 1625 } 1626 } 1627