Home | History | Annotate | Download | only in impl
      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 
     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. */
    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. */
    109   public void showDialpad(boolean animate) {
    110     LogUtil.enterBlock("MainSearchController.showDialpad");
    111     showDialpad(animate, false);
    112   }
    113 
    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    */
    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 
    205   private void hideBottomNav() {
    206     bottomNav.setVisibility(View.GONE);
    207   }
    208 
    209   private void showBottomNav() {
    210     bottomNav.setVisibility(View.VISIBLE);
    211   }
    212 
    213   /** Should be called when {@link DialpadListener#onDialpadShown()} is called. */
    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    */
    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    */
    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    */
    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
    317   protected DialpadFragment getDialpadFragment() {
    318     return (DialpadFragment)
    319         mainActivity.getFragmentManager().findFragmentByTag(DIALPAD_FRAGMENT_TAG);
    320   }
    321 
    322   @Nullable
    323   private NewSearchFragment getSearchFragment() {
    324     return (NewSearchFragment)
    325         mainActivity.getFragmentManager().findFragmentByTag(SEARCH_FRAGMENT_TAG);
    326   }
    327 
    328   private boolean isDialpadVisible() {
    329     DialpadFragment fragment = getDialpadFragment();
    330     return fragment != null
    331         && fragment.isAdded()
    332         && !fragment.isHidden()
    333         && fragment.isDialpadSlideUp();
    334   }
    335 
    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. */
    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
    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 
    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
    389   public void onSearchBackButtonClicked() {
    390     LogUtil.enterBlock("MainSearchController.onSearchBackButtonClicked");
    391     closeSearch(true);
    392   }
    393 
    394   @Override
    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) */
    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
    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
    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
    442   public void onUserLeaveHint() {
    443     if (isInSearch()) {
    444       closeSearch(false);
    445     }
    446   }
    447 
    448   @Override
    449   public void onCallPlacedFromSearch() {
    450     closeSearch(false);
    451   }
    452 
    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 
    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 
    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 
    487   public void addOnSearchShowListener(OnSearchShowListener listener) {
    488     onSearchShowListenerList.add(listener);
    489   }
    490 
    491   public void removeOnSearchShowListener(OnSearchShowListener listener) {
    492     onSearchShowListenerList.remove(listener);
    493   }
    494 
    495   private void notifyListenersOnSearchOpen() {
    496     for (OnSearchShowListener listener : onSearchShowListenerList) {
    497       listener.onSearchOpen();
    498     }
    499   }
    500 
    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 {
    509     void onSearchOpen();
    510 
    511     void onSearchClose();
    512   }
    513 }
    514