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