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