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