1 /*
2  * Copyright (C) 2017 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.main.impl;
18 
19 import android.app.Fragment;
20 import android.app.FragmentTransaction;
21 import android.content.ActivityNotFoundException;
22 import android.content.Intent;
23 import android.os.Bundle;
24 import android.speech.RecognizerIntent;
25 import android.support.annotation.Nullable;
26 import android.support.annotation.VisibleForTesting;
27 import android.support.design.widget.FloatingActionButton;
28 import android.support.v7.app.AppCompatActivity;
29 import android.text.TextUtils;
30 import android.view.MenuItem;
31 import android.view.View;
32 import android.view.animation.Animation;
33 import android.view.animation.Animation.AnimationListener;
34 import android.widget.Toast;
35 import com.android.contacts.common.dialog.ClearFrequentsDialog;
36 import com.android.dialer.app.calllog.CallLogActivity;
37 import com.android.dialer.app.settings.DialerSettingsActivity;
38 import com.android.dialer.callintent.CallInitiationType;
39 import com.android.dialer.common.LogUtil;
40 import com.android.dialer.constants.ActivityRequestCodes;
41 import com.android.dialer.dialpadview.DialpadFragment;
42 import com.android.dialer.dialpadview.DialpadFragment.DialpadListener;
43 import com.android.dialer.dialpadview.DialpadFragment.OnDialpadQueryChangedListener;
44 import com.android.dialer.logging.DialerImpression;
45 import com.android.dialer.logging.Logger;
46 import com.android.dialer.logging.ScreenEvent;
47 import com.android.dialer.main.impl.bottomnav.BottomNavBar;
48 import com.android.dialer.main.impl.toolbar.MainToolbar;
49 import com.android.dialer.main.impl.toolbar.SearchBarListener;
50 import com.android.dialer.searchfragment.list.NewSearchFragment;
51 import com.android.dialer.searchfragment.list.NewSearchFragment.SearchFragmentListener;
52 import com.android.dialer.smartdial.util.SmartDialNameMatcher;
53 import com.android.dialer.util.TransactionSafeActivity;
54 import com.google.common.base.Optional;
55 import java.util.ArrayList;
56 import java.util.List;
57 
58 /**
59  * Search controller for handling all the logic related to entering and exiting the search UI.
60  *
61  * <p>Components modified are:
62  *
63  * <ul>
64  *   <li>Bottom Nav Bar, completely hidden when in search ui.
65  *   <li>FAB, visible in dialpad search when dialpad is hidden. Otherwise, FAB is hidden.
66  *   <li>Toolbar, expanded and visible when dialpad is hidden. Otherwise, hidden off screen.
67  *   <li>Dialpad, shown through fab clicks and hidden with Android back button.
68  * </ul>
69  *
70  * @see #onBackPressed()
71  */
72 public class MainSearchController implements SearchBarListener {
73 
74   private static final String KEY_IS_FAB_HIDDEN = "is_fab_hidden";
75   private static final String KEY_TOOLBAR_SHADOW_VISIBILITY = "toolbar_shadow_visibility";
76   private static final String KEY_IS_TOOLBAR_EXPANDED = "is_toolbar_expanded";
77   private static final String KEY_IS_TOOLBAR_SLIDE_UP = "is_toolbar_slide_up";
78 
79   private static final String DIALPAD_FRAGMENT_TAG = "dialpad_fragment_tag";
80   private static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
81 
82   private final TransactionSafeActivity activity;
83   private final BottomNavBar bottomNav;
84   private final FloatingActionButton fab;
85   private final MainToolbar toolbar;
86   private final View toolbarShadow;
87 
88   /** View located underneath the toolbar that needs to animate with it. */
89   private final View fragmentContainer;
90 
91   private final List<OnSearchShowListener> onSearchShowListenerList = new ArrayList<>();
92 
93   /**
94    * True when an action happens that closes search (like leaving the app or placing a call). We
95    * want to wait until onPause is called otherwise the transition will look extremely janky.
96    */
97   private boolean closeSearchOnPause;
98 
99   private boolean callPlacedFromSearch;
100   private boolean requestingPermission;
101 
102   private DialpadFragment dialpadFragment;
103   private NewSearchFragment searchFragment;
104 
MainSearchController( TransactionSafeActivity activity, BottomNavBar bottomNav, FloatingActionButton fab, MainToolbar toolbar, View toolbarShadow, View fragmentContainer)105   public MainSearchController(
106       TransactionSafeActivity activity,
107       BottomNavBar bottomNav,
108       FloatingActionButton fab,
109       MainToolbar toolbar,
110       View toolbarShadow,
111       View fragmentContainer) {
112     this.activity = activity;
113     this.bottomNav = bottomNav;
114     this.fab = fab;
115     this.toolbar = toolbar;
116     this.toolbarShadow = toolbarShadow;
117     this.fragmentContainer = fragmentContainer;
118 
119     dialpadFragment =
120         (DialpadFragment) activity.getFragmentManager().findFragmentByTag(DIALPAD_FRAGMENT_TAG);
121     searchFragment =
122         (NewSearchFragment) activity.getFragmentManager().findFragmentByTag(SEARCH_FRAGMENT_TAG);
123   }
124 
125   /** Should be called if we're showing the dialpad because of a new ACTION_DIAL intent. */
showDialpadFromNewIntent()126   public void showDialpadFromNewIntent() {
127     LogUtil.enterBlock("MainSearchController.showDialpadFromNewIntent");
128     if (isDialpadVisible()) {
129       // One scenario where this can happen is if the user has the dialpad open when the receive a
130       // call and press add call in the in call ui which calls this method.
131       LogUtil.i("MainSearchController.showDialpadFromNewIntent", "Dialpad is already visible.");
132 
133       // Mark started from new intent in case there is a phone number in the intent
134       dialpadFragment.setStartedFromNewIntent(true);
135       return;
136     }
137     showDialpad(/* animate=*/ false, /* fromNewIntent=*/ true);
138   }
139 
140   /** Shows the dialpad, hides the FAB and slides the toolbar off screen. */
showDialpad(boolean animate)141   public void showDialpad(boolean animate) {
142     LogUtil.enterBlock("MainSearchController.showDialpad");
143     showDialpad(animate, false);
144   }
145 
showDialpad(boolean animate, boolean fromNewIntent)146   private void showDialpad(boolean animate, boolean fromNewIntent) {
147     if (isDialpadVisible()) {
148       LogUtil.e("MainSearchController.showDialpad", "Dialpad is already visible.");
149       return;
150     }
151 
152     Logger.get(activity).logScreenView(ScreenEvent.Type.MAIN_DIALPAD, activity);
153 
154     fab.hide();
155     toolbar.slideUp(animate, fragmentContainer);
156     toolbar.expand(animate, Optional.absent(), /* requestFocus */ false);
157     toolbarShadow.setVisibility(View.VISIBLE);
158 
159     activity.setTitle(R.string.dialpad_activity_title);
160 
161     FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
162 
163     // Show Search
164     if (searchFragment == null) {
165       searchFragment = NewSearchFragment.newInstance();
166       transaction.add(R.id.search_fragment_container, searchFragment, SEARCH_FRAGMENT_TAG);
167       transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
168     } else if (!isSearchVisible()) {
169       transaction.show(searchFragment);
170     }
171 
172     // Show Dialpad
173     if (dialpadFragment == null) {
174       dialpadFragment = new DialpadFragment();
175       dialpadFragment.setStartedFromNewIntent(fromNewIntent);
176       transaction.add(R.id.dialpad_fragment_container, dialpadFragment, DIALPAD_FRAGMENT_TAG);
177       searchFragment.setQuery("", CallInitiationType.Type.DIALPAD);
178     } else {
179       dialpadFragment.setStartedFromNewIntent(fromNewIntent);
180       transaction.show(dialpadFragment);
181     }
182     transaction.commit();
183 
184     notifyListenersOnSearchOpen();
185   }
186 
187   /**
188    * Hides the dialpad, reveals the FAB and slides the toolbar back onto the screen.
189    *
190    * <p>This method intentionally "hides" and does not "remove" the dialpad in order to preserve its
191    * state (i.e. we call {@link FragmentTransaction#hide(Fragment)} instead of {@link
192    * FragmentTransaction#remove(Fragment)}.
193    *
194    * @see {@link #closeSearch(boolean)} to "remove" the dialpad.
195    */
hideDialpad(boolean animate)196   private void hideDialpad(boolean animate) {
197     LogUtil.enterBlock("MainSearchController.hideDialpad");
198     if (dialpadFragment == null) {
199       LogUtil.e("MainSearchController.hideDialpad", "Dialpad fragment is null.");
200       return;
201     }
202 
203     if (!dialpadFragment.isAdded()) {
204       LogUtil.e("MainSearchController.hideDialpad", "Dialpad fragment is not added.");
205       return;
206     }
207 
208     if (dialpadFragment.isHidden()) {
209       LogUtil.e("MainSearchController.hideDialpad", "Dialpad fragment is already hidden.");
210       return;
211     }
212 
213     if (!dialpadFragment.isDialpadSlideUp()) {
214       LogUtil.e("MainSearchController.hideDialpad", "Dialpad fragment is already slide down.");
215       return;
216     }
217 
218     fab.show();
219     toolbar.slideDown(animate, fragmentContainer);
220     toolbar.transferQueryFromDialpad(dialpadFragment.getQuery());
221     activity.setTitle(R.string.main_activity_label);
222 
223     dialpadFragment.setAnimate(animate);
224     dialpadFragment.slideDown(
225         animate,
226         new AnimationListener() {
227           @Override
228           public void onAnimationStart(Animation animation) {}
229 
230           @Override
231           public void onAnimationEnd(Animation animation) {
232             if (activity.isSafeToCommitTransactions()
233                 && !(activity.isFinishing() || activity.isDestroyed())) {
234               activity.getFragmentManager().beginTransaction().hide(dialpadFragment).commit();
235             }
236           }
237 
238           @Override
239           public void onAnimationRepeat(Animation animation) {}
240         });
241   }
242 
hideBottomNav()243   private void hideBottomNav() {
244     bottomNav.setVisibility(View.GONE);
245   }
246 
showBottomNav()247   private void showBottomNav() {
248     bottomNav.setVisibility(View.VISIBLE);
249   }
250 
251   /** Should be called when {@link DialpadListener#onDialpadShown()} is called. */
onDialpadShown()252   public void onDialpadShown() {
253     LogUtil.enterBlock("MainSearchController.onDialpadShown");
254     dialpadFragment.slideUp(true);
255     hideBottomNav();
256   }
257 
258   /**
259    * @see SearchFragmentListener#onSearchListTouch()
260    *     <p>There are 4 scenarios we support to provide a nice UX experience:
261    *     <ol>
262    *       <li>When the dialpad is visible with an empty query, close the search UI.
263    *       <li>When the dialpad is visible with a non-empty query, hide the dialpad.
264    *       <li>When the regular search UI is visible with an empty query, close the search UI.
265    *       <li>When the regular search UI is visible with a non-empty query, hide the keyboard.
266    *     </ol>
267    */
onSearchListTouch()268   public void onSearchListTouch() {
269     LogUtil.enterBlock("MainSearchController.onSearchListTouched");
270     if (isDialpadVisible()) {
271       if (TextUtils.isEmpty(dialpadFragment.getQuery())) {
272         Logger.get(activity)
273             .logImpression(
274                 DialerImpression.Type.MAIN_TOUCH_DIALPAD_SEARCH_LIST_TO_CLOSE_SEARCH_AND_DIALPAD);
275         closeSearch(true);
276       } else {
277         Logger.get(activity)
278             .logImpression(DialerImpression.Type.MAIN_TOUCH_DIALPAD_SEARCH_LIST_TO_HIDE_DIALPAD);
279         hideDialpad(/* animate=*/ true);
280       }
281     } else if (isSearchVisible()) {
282       if (TextUtils.isEmpty(toolbar.getQuery())) {
283         Logger.get(activity)
284             .logImpression(DialerImpression.Type.MAIN_TOUCH_SEARCH_LIST_TO_CLOSE_SEARCH);
285         closeSearch(true);
286       } else {
287         Logger.get(activity)
288             .logImpression(DialerImpression.Type.MAIN_TOUCH_SEARCH_LIST_TO_HIDE_KEYBOARD);
289         closeKeyboard();
290       }
291     }
292   }
293 
294   /**
295    * Should be called when the user presses the back button.
296    *
297    * @return true if #onBackPressed() handled to action.
298    */
onBackPressed()299   public boolean onBackPressed() {
300     if (isDialpadVisible() && !TextUtils.isEmpty(dialpadFragment.getQuery())) {
301       LogUtil.i("MainSearchController.onBackPressed", "Dialpad visible with query");
302       Logger.get(activity)
303           .logImpression(DialerImpression.Type.MAIN_PRESS_BACK_BUTTON_TO_HIDE_DIALPAD);
304       hideDialpad(/* animate=*/ true);
305       return true;
306     } else if (isSearchVisible()) {
307       LogUtil.i("MainSearchController.onBackPressed", "Search is visible");
308       Logger.get(activity)
309           .logImpression(
310               isDialpadVisible()
311                   ? DialerImpression.Type.MAIN_PRESS_BACK_BUTTON_TO_CLOSE_SEARCH_AND_DIALPAD
312                   : DialerImpression.Type.MAIN_PRESS_BACK_BUTTON_TO_CLOSE_SEARCH);
313       closeSearch(true);
314       return true;
315     } else {
316       return false;
317     }
318   }
319 
320   /** Calls {@link #hideDialpad(boolean)}, removes the search fragment and clears the dialpad. */
closeSearch(boolean animate)321   private void closeSearch(boolean animate) {
322     LogUtil.enterBlock("MainSearchController.closeSearch");
323     if (searchFragment == null) {
324       LogUtil.e("MainSearchController.closeSearch", "Search fragment is null.");
325       return;
326     }
327 
328     if (!searchFragment.isAdded()) {
329       LogUtil.e("MainSearchController.closeSearch", "Search fragment isn't added.");
330       return;
331     }
332 
333     if (searchFragment.isHidden()) {
334       LogUtil.e("MainSearchController.closeSearch", "Search fragment is already hidden.");
335       return;
336     }
337 
338     if (isDialpadVisible()) {
339       hideDialpad(animate);
340     } else if (!fab.isShown()) {
341       fab.show();
342     }
343     showBottomNav();
344     toolbar.collapse(animate);
345     toolbarShadow.setVisibility(View.GONE);
346     activity.getFragmentManager().beginTransaction().hide(searchFragment).commit();
347 
348     // Clear the dialpad so the phone number isn't persisted between search sessions.
349     if (dialpadFragment != null) {
350       // Temporarily disable accessibility when we clear the dialpad, since it should be
351       // invisible and should not announce anything.
352       dialpadFragment
353           .getDigitsWidget()
354           .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
355       dialpadFragment.clearDialpad();
356       dialpadFragment
357           .getDigitsWidget()
358           .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
359     }
360 
361     notifyListenersOnSearchClose();
362   }
363 
364   @Nullable
getDialpadFragment()365   protected DialpadFragment getDialpadFragment() {
366     return dialpadFragment;
367   }
368 
isDialpadVisible()369   private boolean isDialpadVisible() {
370     return dialpadFragment != null
371         && dialpadFragment.isAdded()
372         && !dialpadFragment.isHidden()
373         && dialpadFragment.isDialpadSlideUp();
374   }
375 
isSearchVisible()376   private boolean isSearchVisible() {
377     return searchFragment != null && searchFragment.isAdded() && !searchFragment.isHidden();
378   }
379 
380   /** Returns true if the search UI is visible. */
isInSearch()381   public boolean isInSearch() {
382     return isSearchVisible();
383   }
384 
385   /** Closes the keyboard if necessary. */
closeKeyboard()386   private void closeKeyboard() {
387     if (searchFragment != null && searchFragment.isAdded()) {
388       toolbar.hideKeyboard();
389     }
390   }
391 
392   /**
393    * Opens search in regular/search bar search mode.
394    *
395    * <p>Hides fab, expands toolbar and starts the search fragment.
396    */
397   @Override
onSearchBarClicked()398   public void onSearchBarClicked() {
399     LogUtil.enterBlock("MainSearchController.onSearchBarClicked");
400     Logger.get(activity).logImpression(DialerImpression.Type.MAIN_CLICK_SEARCH_BAR);
401     openSearch(Optional.absent());
402   }
403 
openSearch(Optional<String> query)404   private void openSearch(Optional<String> query) {
405     LogUtil.enterBlock("MainSearchController.openSearch");
406 
407     Logger.get(activity).logScreenView(ScreenEvent.Type.MAIN_SEARCH, activity);
408 
409     fab.hide();
410     toolbar.expand(/* animate=*/ true, query, /* requestFocus */ true);
411     toolbar.showKeyboard();
412     toolbarShadow.setVisibility(View.VISIBLE);
413     hideBottomNav();
414 
415     FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
416     // Show Search
417     if (searchFragment == null) {
418       searchFragment = NewSearchFragment.newInstance();
419       transaction.add(R.id.search_fragment_container, searchFragment, SEARCH_FRAGMENT_TAG);
420       transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
421     } else if (!isSearchVisible()) {
422       transaction.show(searchFragment);
423     }
424 
425     searchFragment.setQuery(
426         query.isPresent() ? query.get() : "", CallInitiationType.Type.REGULAR_SEARCH);
427 
428     if (activity.isSafeToCommitTransactions()) {
429       transaction.commit();
430     }
431 
432     notifyListenersOnSearchOpen();
433   }
434 
435   @Override
onSearchBackButtonClicked()436   public void onSearchBackButtonClicked() {
437     LogUtil.enterBlock("MainSearchController.onSearchBackButtonClicked");
438     closeSearch(true);
439   }
440 
441   @Override
onSearchQueryUpdated(String query)442   public void onSearchQueryUpdated(String query) {
443     if (searchFragment != null) {
444       searchFragment.setQuery(query, CallInitiationType.Type.REGULAR_SEARCH);
445     }
446   }
447 
448   /** @see OnDialpadQueryChangedListener#onDialpadQueryChanged(java.lang.String) */
onDialpadQueryChanged(String query)449   public void onDialpadQueryChanged(String query) {
450     String normalizedQuery = SmartDialNameMatcher.normalizeNumber(/* context = */ activity, query);
451     if (searchFragment != null) {
452       searchFragment.setRawNumber(query);
453       searchFragment.setQuery(normalizedQuery, CallInitiationType.Type.DIALPAD);
454     }
455     dialpadFragment.process_quote_emergency_unquote(normalizedQuery);
456   }
457 
458   @Override
onVoiceButtonClicked(VoiceSearchResultCallback voiceSearchResultCallback)459   public void onVoiceButtonClicked(VoiceSearchResultCallback voiceSearchResultCallback) {
460     Logger.get(activity).logImpression(DialerImpression.Type.MAIN_CLICK_SEARCH_BAR_VOICE_BUTTON);
461     try {
462       Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
463       activity.startActivityForResult(voiceIntent, ActivityRequestCodes.DIALTACTS_VOICE_SEARCH);
464     } catch (ActivityNotFoundException e) {
465       Toast.makeText(activity, R.string.voice_search_not_available, Toast.LENGTH_SHORT).show();
466     }
467   }
468 
469   @Override
onMenuItemClicked(MenuItem menuItem)470   public boolean onMenuItemClicked(MenuItem menuItem) {
471     if (menuItem.getItemId() == R.id.settings) {
472       activity.startActivity(new Intent(activity, DialerSettingsActivity.class));
473       Logger.get(activity).logScreenView(ScreenEvent.Type.SETTINGS, activity);
474       return true;
475     } else if (menuItem.getItemId() == R.id.clear_frequents) {
476       ClearFrequentsDialog.show(activity.getFragmentManager());
477       Logger.get(activity).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, activity);
478       return true;
479     } else if (menuItem.getItemId() == R.id.menu_call_history) {
480       final Intent intent = new Intent(activity, CallLogActivity.class);
481       activity.startActivity(intent);
482     }
483     return false;
484   }
485 
486   @Override
onActivityPause()487   public void onActivityPause() {
488     LogUtil.enterBlock("MainSearchController.onActivityPause");
489     closeKeyboard();
490 
491     if (closeSearchOnPause) {
492       if (isInSearch() && (callPlacedFromSearch || !isDialpadVisible())) {
493         closeSearch(false);
494       }
495       closeSearchOnPause = false;
496       callPlacedFromSearch = false;
497     }
498   }
499 
500   @Override
onUserLeaveHint()501   public void onUserLeaveHint() {
502     if (isInSearch()) {
503       // Requesting a permission causes this to be called and we want search to remain open when
504       // that happens. Otherwise, close search.
505       closeSearchOnPause = !requestingPermission;
506 
507       // Always hide the keyboard when the user leaves dialer (including permission requests)
508       closeKeyboard();
509     }
510   }
511 
512   @Override
onCallPlacedFromSearch()513   public void onCallPlacedFromSearch() {
514     closeSearchOnPause = true;
515     callPlacedFromSearch = true;
516   }
517 
518   @Override
requestingPermission()519   public void requestingPermission() {
520     LogUtil.enterBlock("MainSearchController.requestingPermission");
521     requestingPermission = true;
522   }
523 
onVoiceResults(int resultCode, Intent data)524   public void onVoiceResults(int resultCode, Intent data) {
525     if (resultCode == AppCompatActivity.RESULT_OK) {
526       ArrayList<String> matches = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
527       if (matches.size() > 0) {
528         LogUtil.i("MainSearchController.onVoiceResults", "voice search - match found");
529         openSearch(Optional.of(matches.get(0)));
530       } else {
531         LogUtil.i("MainSearchController.onVoiceResults", "voice search - nothing heard");
532       }
533     } else {
534       LogUtil.e("MainSearchController.onVoiceResults", "voice search failed");
535     }
536   }
537 
onSaveInstanceState(Bundle bundle)538   public void onSaveInstanceState(Bundle bundle) {
539     bundle.putBoolean(KEY_IS_FAB_HIDDEN, !fab.isShown());
540     bundle.putInt(KEY_TOOLBAR_SHADOW_VISIBILITY, toolbarShadow.getVisibility());
541     bundle.putBoolean(KEY_IS_TOOLBAR_EXPANDED, toolbar.isExpanded());
542     bundle.putBoolean(KEY_IS_TOOLBAR_SLIDE_UP, toolbar.isSlideUp());
543   }
544 
onRestoreInstanceState(Bundle savedInstanceState)545   public void onRestoreInstanceState(Bundle savedInstanceState) {
546     toolbarShadow.setVisibility(savedInstanceState.getInt(KEY_TOOLBAR_SHADOW_VISIBILITY));
547     if (savedInstanceState.getBoolean(KEY_IS_FAB_HIDDEN, false)) {
548       fab.hide();
549     }
550     boolean isSlideUp = savedInstanceState.getBoolean(KEY_IS_TOOLBAR_SLIDE_UP, false);
551     if (isSlideUp) {
552       toolbar.slideUp(false, fragmentContainer);
553     }
554     if (savedInstanceState.getBoolean(KEY_IS_TOOLBAR_EXPANDED, false)) {
555       // If the toolbar is slide up, that means the dialpad is showing. Thus we don't want to
556       // request focus or we'll break physical/bluetooth keyboards typing.
557       toolbar.expand(/* animate */ false, Optional.absent(), /* requestFocus */ !isSlideUp);
558     }
559   }
560 
addOnSearchShowListener(OnSearchShowListener listener)561   public void addOnSearchShowListener(OnSearchShowListener listener) {
562     onSearchShowListenerList.add(listener);
563   }
564 
removeOnSearchShowListener(OnSearchShowListener listener)565   public void removeOnSearchShowListener(OnSearchShowListener listener) {
566     onSearchShowListenerList.remove(listener);
567   }
568 
notifyListenersOnSearchOpen()569   private void notifyListenersOnSearchOpen() {
570     for (OnSearchShowListener listener : onSearchShowListenerList) {
571       listener.onSearchOpen();
572     }
573   }
574 
notifyListenersOnSearchClose()575   private void notifyListenersOnSearchClose() {
576     for (OnSearchShowListener listener : onSearchShowListenerList) {
577       listener.onSearchClose();
578     }
579   }
580 
581   /** Listener for search fragment show states change */
582   public interface OnSearchShowListener {
onSearchOpen()583     void onSearchOpen();
584 
onSearchClose()585     void onSearchClose();
586   }
587 
588   @VisibleForTesting
setDialpadFragment(DialpadFragment dialpadFragment)589   void setDialpadFragment(DialpadFragment dialpadFragment) {
590     this.dialpadFragment = dialpadFragment;
591   }
592 
593   @VisibleForTesting
setSearchFragment(NewSearchFragment searchFragment)594   void setSearchFragment(NewSearchFragment searchFragment) {
595     this.searchFragment = searchFragment;
596   }
597 }
598