• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.design.widget.FloatingActionButton;
27 import android.support.v7.app.AppCompatActivity;
28 import android.text.TextUtils;
29 import android.view.MenuItem;
30 import android.view.View;
31 import android.view.animation.Animation;
32 import android.view.animation.Animation.AnimationListener;
33 import android.widget.Toast;
34 import com.android.contacts.common.dialog.ClearFrequentsDialog;
35 import com.android.dialer.app.calllog.CallLogActivity;
36 import com.android.dialer.app.settings.DialerSettingsActivity;
37 import com.android.dialer.callintent.CallInitiationType;
38 import com.android.dialer.common.Assert;
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.google.common.base.Optional;
54 import java.util.ArrayList;
55 import java.util.List;
56 
57 /**
58  * Search controller for handling all the logic related to entering and exiting the search UI.
59  *
60  * <p>Components modified are:
61  *
62  * <ul>
63  *   <li>Bottom Nav Bar, completely hidden when in search ui.
64  *   <li>FAB, visible in dialpad search when dialpad is hidden. Otherwise, FAB is hidden.
65  *   <li>Toolbar, expanded and visible when dialpad is hidden. Otherwise, hidden off screen.
66  *   <li>Dialpad, shown through fab clicks and hidden with Android back button.
67  * </ul>
68  *
69  * @see #onBackPressed()
70  */
71 public class MainSearchController implements SearchBarListener {
72 
73   private static final String KEY_IS_FAB_HIDDEN = "is_fab_hidden";
74   private static final String KEY_TOOLBAR_SHADOW_VISIBILITY = "toolbar_shadow_visibility";
75   private static final String KEY_IS_TOOLBAR_EXPANDED = "is_toolbar_expanded";
76   private static final String KEY_IS_TOOLBAR_SLIDE_UP = "is_toolbar_slide_up";
77 
78   private static final String DIALPAD_FRAGMENT_TAG = "dialpad_fragment_tag";
79   private static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
80 
81   private final MainActivity mainActivity;
82   private final BottomNavBar bottomNav;
83   private final FloatingActionButton fab;
84   private final MainToolbar toolbar;
85   private final View toolbarShadow;
86 
87   private final List<OnSearchShowListener> onSearchShowListenerList = new ArrayList<>();
88 
MainSearchController( MainActivity mainActivity, BottomNavBar bottomNav, FloatingActionButton fab, MainToolbar toolbar, View toolbarShadow)89   public MainSearchController(
90       MainActivity mainActivity,
91       BottomNavBar bottomNav,
92       FloatingActionButton fab,
93       MainToolbar toolbar,
94       View toolbarShadow) {
95     this.mainActivity = mainActivity;
96     this.bottomNav = bottomNav;
97     this.fab = fab;
98     this.toolbar = toolbar;
99     this.toolbarShadow = toolbarShadow;
100   }
101 
102   /** Should be called if we're showing the dialpad because of a new ACTION_DIAL intent. */
showDialpadFromNewIntent()103   public void showDialpadFromNewIntent() {
104     LogUtil.enterBlock("MainSearchController.showDialpadFromNewIntent");
105     showDialpad(/* animate=*/ false, /* fromNewIntent=*/ true);
106   }
107 
108   /** Shows the dialpad, hides the FAB and slides the toolbar off screen. */
showDialpad(boolean animate)109   public void showDialpad(boolean animate) {
110     LogUtil.enterBlock("MainSearchController.showDialpad");
111     showDialpad(animate, false);
112   }
113 
showDialpad(boolean animate, boolean fromNewIntent)114   private void showDialpad(boolean animate, boolean fromNewIntent) {
115     Assert.checkArgument(!isDialpadVisible());
116 
117     fab.hide();
118     toolbar.slideUp(animate);
119     toolbar.expand(animate, Optional.absent());
120     toolbarShadow.setVisibility(View.VISIBLE);
121     mainActivity.setTitle(R.string.dialpad_activity_title);
122 
123     FragmentTransaction transaction = mainActivity.getFragmentManager().beginTransaction();
124     NewSearchFragment searchFragment = getSearchFragment();
125 
126     // Show Search
127     if (searchFragment == null) {
128       // TODO(a bug): zero suggest results aren't actually shown but this enabled the nearby
129       // places promo to be shown.
130       searchFragment = NewSearchFragment.newInstance(/* showZeroSuggest=*/ true);
131       transaction.replace(R.id.fragment_container, searchFragment, SEARCH_FRAGMENT_TAG);
132       transaction.addToBackStack(null);
133       transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
134     } else if (!isSearchVisible()) {
135       transaction.show(searchFragment);
136     }
137     searchFragment.setQuery("", CallInitiationType.Type.DIALPAD);
138 
139     // Split the transactions so that the dialpad fragment isn't popped off the stack when we exit
140     // search. We do this so that the dialpad actually animates down instead of just disappearing.
141     transaction.commit();
142     transaction = mainActivity.getFragmentManager().beginTransaction();
143 
144     // Show Dialpad
145     if (getDialpadFragment() == null) {
146       DialpadFragment dialpadFragment = new DialpadFragment();
147       dialpadFragment.setStartedFromNewIntent(fromNewIntent);
148       transaction.add(R.id.dialpad_fragment_container, dialpadFragment, DIALPAD_FRAGMENT_TAG);
149     } else {
150       DialpadFragment dialpadFragment = getDialpadFragment();
151       dialpadFragment.setStartedFromNewIntent(fromNewIntent);
152       transaction.show(dialpadFragment);
153     }
154     transaction.commit();
155 
156     notifyListenersOnSearchOpen();
157   }
158 
159   /**
160    * Hides the dialpad, reveals the FAB and slides the toolbar back onto the screen.
161    *
162    * <p>This method intentionally "hides" and does not "remove" the dialpad in order to preserve its
163    * state (i.e. we call {@link FragmentTransaction#hide(Fragment)} instead of {@link
164    * FragmentTransaction#remove(Fragment)}.
165    *
166    * @see {@link #closeSearch(boolean)} to "remove" the dialpad.
167    */
hideDialpad(boolean animate, boolean bottomNavVisible)168   private void hideDialpad(boolean animate, boolean bottomNavVisible) {
169     LogUtil.enterBlock("MainSearchController.hideDialpad");
170     Assert.checkArgument(isDialpadVisible());
171 
172     fab.show();
173     toolbar.slideDown(animate);
174     toolbar.transferQueryFromDialpad(getDialpadFragment().getQuery());
175     mainActivity.setTitle(R.string.main_activity_label);
176 
177     DialpadFragment dialpadFragment = getDialpadFragment();
178     dialpadFragment.setAnimate(animate);
179     dialpadFragment.slideDown(
180         animate,
181         new AnimationListener() {
182           @Override
183           public void onAnimationStart(Animation animation) {
184             // Slide the bottom nav on animation start so it's (not) visible when the dialpad
185             // finishes animating down.
186             if (bottomNavVisible) {
187               showBottomNav();
188             } else {
189               hideBottomNav();
190             }
191           }
192 
193           @Override
194           public void onAnimationEnd(Animation animation) {
195             if (!(mainActivity.isFinishing() || mainActivity.isDestroyed())) {
196               mainActivity.getFragmentManager().beginTransaction().hide(dialpadFragment).commit();
197             }
198           }
199 
200           @Override
201           public void onAnimationRepeat(Animation animation) {}
202         });
203   }
204 
hideBottomNav()205   private void hideBottomNav() {
206     bottomNav.setVisibility(View.GONE);
207   }
208 
showBottomNav()209   private void showBottomNav() {
210     bottomNav.setVisibility(View.VISIBLE);
211   }
212 
213   /** Should be called when {@link DialpadListener#onDialpadShown()} is called. */
onDialpadShown()214   public void onDialpadShown() {
215     LogUtil.enterBlock("MainSearchController.onDialpadShown");
216     getDialpadFragment().slideUp(true);
217     hideBottomNav();
218   }
219 
220   /**
221    * @see SearchFragmentListener#onSearchListTouch()
222    *     <p>There are 4 scenarios we support to provide a nice UX experience:
223    *     <ol>
224    *       <li>When the dialpad is visible with an empty query, close the search UI.
225    *       <li>When the dialpad is visible with a non-empty query, hide the dialpad.
226    *       <li>When the regular search UI is visible with an empty query, close the search UI.
227    *       <li>When the regular search UI is visible with a non-empty query, hide the keyboard.
228    *     </ol>
229    */
onSearchListTouch()230   public void onSearchListTouch() {
231     LogUtil.enterBlock("MainSearchController.onSearchListTouched");
232     if (isDialpadVisible()) {
233       if (TextUtils.isEmpty(getDialpadFragment().getQuery())) {
234         Logger.get(mainActivity)
235             .logImpression(
236                 DialerImpression.Type.MAIN_TOUCH_DIALPAD_SEARCH_LIST_TO_CLOSE_SEARCH_AND_DIALPAD);
237         closeSearch(true);
238       } else {
239         Logger.get(mainActivity)
240             .logImpression(DialerImpression.Type.MAIN_TOUCH_DIALPAD_SEARCH_LIST_TO_HIDE_DIALPAD);
241         hideDialpad(/* animate=*/ true, /* bottomNavVisible=*/ false);
242       }
243     } else if (isSearchVisible()) {
244       if (TextUtils.isEmpty(toolbar.getQuery())) {
245         Logger.get(mainActivity)
246             .logImpression(DialerImpression.Type.MAIN_TOUCH_SEARCH_LIST_TO_CLOSE_SEARCH);
247         closeSearch(true);
248       } else {
249         Logger.get(mainActivity)
250             .logImpression(DialerImpression.Type.MAIN_TOUCH_SEARCH_LIST_TO_HIDE_KEYBOARD);
251         toolbar.hideKeyboard();
252       }
253     }
254   }
255 
256   /**
257    * Should be called when the user presses the back button.
258    *
259    * @return true if #onBackPressed() handled to action.
260    */
onBackPressed()261   public boolean onBackPressed() {
262     if (isDialpadVisible() && !TextUtils.isEmpty(getDialpadFragment().getQuery())) {
263       LogUtil.i("MainSearchController.onBackPressed", "Dialpad visible with query");
264       Logger.get(mainActivity)
265           .logImpression(DialerImpression.Type.MAIN_PRESS_BACK_BUTTON_TO_HIDE_DIALPAD);
266       hideDialpad(/* animate=*/ true, /* bottomNavVisible=*/ false);
267       return true;
268     } else if (isSearchVisible()) {
269       LogUtil.i("MainSearchController.onBackPressed", "Search is visible");
270       Logger.get(mainActivity)
271           .logImpression(
272               isDialpadVisible()
273                   ? DialerImpression.Type.MAIN_PRESS_BACK_BUTTON_TO_CLOSE_SEARCH_AND_DIALPAD
274                   : DialerImpression.Type.MAIN_PRESS_BACK_BUTTON_TO_CLOSE_SEARCH);
275       closeSearch(true);
276       return true;
277     } else {
278       return false;
279     }
280   }
281 
282   /**
283    * Calls {@link #hideDialpad(boolean, boolean)}, removes the search fragment and clears the
284    * dialpad.
285    */
closeSearch(boolean animate)286   private void closeSearch(boolean animate) {
287     LogUtil.enterBlock("MainSearchController.closeSearch");
288     Assert.checkArgument(isSearchVisible());
289     if (isDialpadVisible()) {
290       hideDialpad(animate, /* bottomNavVisible=*/ true);
291     } else if (!fab.isShown()) {
292       fab.show();
293     }
294     showBottomNav();
295     toolbar.collapse(animate);
296     toolbarShadow.setVisibility(View.GONE);
297     mainActivity.getFragmentManager().popBackStack();
298 
299     // Clear the dialpad so the phone number isn't persisted between search sessions.
300     DialpadFragment dialpadFragment = getDialpadFragment();
301     if (dialpadFragment != null) {
302       // Temporarily disable accessibility when we clear the dialpad, since it should be
303       // invisible and should not announce anything.
304       dialpadFragment
305           .getDigitsWidget()
306           .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
307       dialpadFragment.clearDialpad();
308       dialpadFragment
309           .getDigitsWidget()
310           .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
311     }
312 
313     notifyListenersOnSearchClose();
314   }
315 
316   @Nullable
getDialpadFragment()317   protected DialpadFragment getDialpadFragment() {
318     return (DialpadFragment)
319         mainActivity.getFragmentManager().findFragmentByTag(DIALPAD_FRAGMENT_TAG);
320   }
321 
322   @Nullable
getSearchFragment()323   private NewSearchFragment getSearchFragment() {
324     return (NewSearchFragment)
325         mainActivity.getFragmentManager().findFragmentByTag(SEARCH_FRAGMENT_TAG);
326   }
327 
isDialpadVisible()328   private boolean isDialpadVisible() {
329     DialpadFragment fragment = getDialpadFragment();
330     return fragment != null
331         && fragment.isAdded()
332         && !fragment.isHidden()
333         && fragment.isDialpadSlideUp();
334   }
335 
isSearchVisible()336   private boolean isSearchVisible() {
337     NewSearchFragment fragment = getSearchFragment();
338     return fragment != null && fragment.isAdded() && !fragment.isHidden();
339   }
340 
341   /** Returns true if the search UI is visible. */
isInSearch()342   public boolean isInSearch() {
343     return isSearchVisible();
344   }
345 
346   /**
347    * Opens search in regular/search bar search mode.
348    *
349    * <p>Hides fab, expands toolbar and starts the search fragment.
350    */
351   @Override
onSearchBarClicked()352   public void onSearchBarClicked() {
353     LogUtil.enterBlock("MainSearchController.onSearchBarClicked");
354     Logger.get(mainActivity).logImpression(DialerImpression.Type.MAIN_CLICK_SEARCH_BAR);
355     openSearch(Optional.absent());
356   }
357 
openSearch(Optional<String> query)358   private void openSearch(Optional<String> query) {
359     LogUtil.enterBlock("MainSearchController.openSearch");
360     fab.hide();
361     toolbar.expand(/* animate=*/ true, query);
362     toolbar.showKeyboard();
363     toolbarShadow.setVisibility(View.VISIBLE);
364     hideBottomNav();
365 
366     FragmentTransaction transaction = mainActivity.getFragmentManager().beginTransaction();
367     NewSearchFragment searchFragment = getSearchFragment();
368 
369     // Show Search
370     if (searchFragment == null) {
371       // TODO(a bug): zero suggest results aren't actually shown but this enabled the nearby
372       // places promo to be shown.
373       searchFragment = NewSearchFragment.newInstance(true);
374       transaction.replace(R.id.fragment_container, searchFragment, SEARCH_FRAGMENT_TAG);
375       transaction.addToBackStack(null);
376       transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
377     } else if (!isSearchVisible()) {
378       transaction.show(getSearchFragment());
379     }
380 
381     searchFragment.setQuery(
382         query.isPresent() ? query.get() : "", CallInitiationType.Type.REGULAR_SEARCH);
383     transaction.commit();
384 
385     notifyListenersOnSearchOpen();
386   }
387 
388   @Override
onSearchBackButtonClicked()389   public void onSearchBackButtonClicked() {
390     LogUtil.enterBlock("MainSearchController.onSearchBackButtonClicked");
391     closeSearch(true);
392   }
393 
394   @Override
onSearchQueryUpdated(String query)395   public void onSearchQueryUpdated(String query) {
396     NewSearchFragment fragment = getSearchFragment();
397     if (fragment != null) {
398       fragment.setQuery(query, CallInitiationType.Type.REGULAR_SEARCH);
399     }
400   }
401 
402   /** @see OnDialpadQueryChangedListener#onDialpadQueryChanged(java.lang.String) */
onDialpadQueryChanged(String query)403   public void onDialpadQueryChanged(String query) {
404     query = SmartDialNameMatcher.normalizeNumber(/* context = */ mainActivity, query);
405     NewSearchFragment fragment = getSearchFragment();
406     if (fragment != null) {
407       fragment.setQuery(query, CallInitiationType.Type.DIALPAD);
408     }
409     getDialpadFragment().process_quote_emergency_unquote(query);
410   }
411 
412   @Override
onVoiceButtonClicked(VoiceSearchResultCallback voiceSearchResultCallback)413   public void onVoiceButtonClicked(VoiceSearchResultCallback voiceSearchResultCallback) {
414     Logger.get(mainActivity)
415         .logImpression(DialerImpression.Type.MAIN_CLICK_SEARCH_BAR_VOICE_BUTTON);
416     try {
417       Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
418       mainActivity.startActivityForResult(voiceIntent, ActivityRequestCodes.DIALTACTS_VOICE_SEARCH);
419     } catch (ActivityNotFoundException e) {
420       Toast.makeText(mainActivity, R.string.voice_search_not_available, Toast.LENGTH_SHORT).show();
421     }
422   }
423 
424   @Override
onMenuItemClicked(MenuItem menuItem)425   public boolean onMenuItemClicked(MenuItem menuItem) {
426     if (menuItem.getItemId() == R.id.settings) {
427       mainActivity.startActivity(new Intent(mainActivity, DialerSettingsActivity.class));
428       Logger.get(mainActivity).logScreenView(ScreenEvent.Type.SETTINGS, mainActivity);
429       return true;
430     } else if (menuItem.getItemId() == R.id.clear_frequents) {
431       ClearFrequentsDialog.show(mainActivity.getFragmentManager());
432       Logger.get(mainActivity).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, mainActivity);
433       return true;
434     } else if (menuItem.getItemId() == R.id.menu_call_history) {
435       final Intent intent = new Intent(mainActivity, CallLogActivity.class);
436       mainActivity.startActivity(intent);
437     }
438     return false;
439   }
440 
441   @Override
onUserLeaveHint()442   public void onUserLeaveHint() {
443     if (isInSearch()) {
444       closeSearch(false);
445     }
446   }
447 
448   @Override
onCallPlacedFromSearch()449   public void onCallPlacedFromSearch() {
450     closeSearch(false);
451   }
452 
onVoiceResults(int resultCode, Intent data)453   public void onVoiceResults(int resultCode, Intent data) {
454     if (resultCode == AppCompatActivity.RESULT_OK) {
455       ArrayList<String> matches = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
456       if (matches.size() > 0) {
457         LogUtil.i("MainSearchController.onVoiceResults", "voice search - match found");
458         openSearch(Optional.of(matches.get(0)));
459       } else {
460         LogUtil.i("MainSearchController.onVoiceResults", "voice search - nothing heard");
461       }
462     } else {
463       LogUtil.e("MainSearchController.onVoiceResults", "voice search failed");
464     }
465   }
466 
onSaveInstanceState(Bundle bundle)467   public void onSaveInstanceState(Bundle bundle) {
468     bundle.putBoolean(KEY_IS_FAB_HIDDEN, !fab.isShown());
469     bundle.putInt(KEY_TOOLBAR_SHADOW_VISIBILITY, toolbarShadow.getVisibility());
470     bundle.putBoolean(KEY_IS_TOOLBAR_EXPANDED, toolbar.isExpanded());
471     bundle.putBoolean(KEY_IS_TOOLBAR_SLIDE_UP, toolbar.isSlideUp());
472   }
473 
onRestoreInstanceState(Bundle savedInstanceState)474   public void onRestoreInstanceState(Bundle savedInstanceState) {
475     toolbarShadow.setVisibility(savedInstanceState.getInt(KEY_TOOLBAR_SHADOW_VISIBILITY));
476     if (savedInstanceState.getBoolean(KEY_IS_FAB_HIDDEN, false)) {
477       fab.hide();
478     }
479     if (savedInstanceState.getBoolean(KEY_IS_TOOLBAR_EXPANDED, false)) {
480       toolbar.expand(false, Optional.absent());
481     }
482     if (savedInstanceState.getBoolean(KEY_IS_TOOLBAR_SLIDE_UP, false)) {
483       toolbar.slideUp(false);
484     }
485   }
486 
addOnSearchShowListener(OnSearchShowListener listener)487   public void addOnSearchShowListener(OnSearchShowListener listener) {
488     onSearchShowListenerList.add(listener);
489   }
490 
removeOnSearchShowListener(OnSearchShowListener listener)491   public void removeOnSearchShowListener(OnSearchShowListener listener) {
492     onSearchShowListenerList.remove(listener);
493   }
494 
notifyListenersOnSearchOpen()495   private void notifyListenersOnSearchOpen() {
496     for (OnSearchShowListener listener : onSearchShowListenerList) {
497       listener.onSearchOpen();
498     }
499   }
500 
notifyListenersOnSearchClose()501   private void notifyListenersOnSearchClose() {
502     for (OnSearchShowListener listener : onSearchShowListenerList) {
503       listener.onSearchClose();
504     }
505   }
506 
507   /** Listener for search fragment show states change */
508   public interface OnSearchShowListener {
onSearchOpen()509     void onSearchOpen();
510 
onSearchClose()511     void onSearchClose();
512   }
513 }
514