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