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