/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.dialer.main.impl;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Bundle;
import android.speech.RecognizerIntent;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.widget.Toast;
import com.android.contacts.common.dialog.ClearFrequentsDialog;
import com.android.dialer.app.calllog.CallLogActivity;
import com.android.dialer.app.settings.DialerSettingsActivity;
import com.android.dialer.callintent.CallInitiationType;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.constants.ActivityRequestCodes;
import com.android.dialer.dialpadview.DialpadFragment;
import com.android.dialer.dialpadview.DialpadFragment.DialpadListener;
import com.android.dialer.dialpadview.DialpadFragment.OnDialpadQueryChangedListener;
import com.android.dialer.logging.DialerImpression;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.ScreenEvent;
import com.android.dialer.main.impl.bottomnav.BottomNavBar;
import com.android.dialer.main.impl.toolbar.MainToolbar;
import com.android.dialer.main.impl.toolbar.SearchBarListener;
import com.android.dialer.searchfragment.list.NewSearchFragment;
import com.android.dialer.searchfragment.list.NewSearchFragment.SearchFragmentListener;
import com.android.dialer.smartdial.util.SmartDialNameMatcher;
import com.google.common.base.Optional;
import java.util.ArrayList;
import java.util.List;
/**
* Search controller for handling all the logic related to entering and exiting the search UI.
*
*
onSearchShowListenerList = new ArrayList<>();
/**
* True when an action happens that closes search (like leaving the app or placing a call). We
* want to wait until onPause is called otherwise the transition will look extremely janky.
*/
private boolean closeSearchOnPause;
private boolean requestingPermission;
public MainSearchController(
AppCompatActivity activity,
BottomNavBar bottomNav,
FloatingActionButton fab,
MainToolbar toolbar,
View toolbarShadow,
View fragmentContainer) {
this.activity = activity;
this.bottomNav = bottomNav;
this.fab = fab;
this.toolbar = toolbar;
this.toolbarShadow = toolbarShadow;
this.fragmentContainer = fragmentContainer;
}
/** Should be called if we're showing the dialpad because of a new ACTION_DIAL intent. */
public void showDialpadFromNewIntent() {
LogUtil.enterBlock("MainSearchController.showDialpadFromNewIntent");
if (isDialpadVisible()) {
// One scenario where this can happen is if the user has the dialpad open when the receive a
// call and press add call in the in call ui which calls this method.
LogUtil.i("MainSearchController.showDialpadFromNewIntent", "Dialpad is already visible.");
// Mark started from new intent in case there is a phone number in the intent
getDialpadFragment().setStartedFromNewIntent(true);
return;
}
showDialpad(/* animate=*/ false, /* fromNewIntent=*/ true);
}
/** Shows the dialpad, hides the FAB and slides the toolbar off screen. */
public void showDialpad(boolean animate) {
LogUtil.enterBlock("MainSearchController.showDialpad");
showDialpad(animate, false);
}
private void showDialpad(boolean animate, boolean fromNewIntent) {
Assert.checkArgument(!isDialpadVisible());
Logger.get(activity).logScreenView(ScreenEvent.Type.MAIN_DIALPAD, activity);
fab.hide();
toolbar.slideUp(animate, fragmentContainer);
toolbar.expand(animate, Optional.absent());
toolbarShadow.setVisibility(View.VISIBLE);
activity.setTitle(R.string.dialpad_activity_title);
FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
NewSearchFragment searchFragment = getSearchFragment();
// Show Search
if (searchFragment == null) {
searchFragment = NewSearchFragment.newInstance();
transaction.add(R.id.search_fragment_container, searchFragment, SEARCH_FRAGMENT_TAG);
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
} else if (!isSearchVisible()) {
transaction.show(searchFragment);
}
// Show Dialpad
if (getDialpadFragment() == null) {
DialpadFragment dialpadFragment = new DialpadFragment();
dialpadFragment.setStartedFromNewIntent(fromNewIntent);
transaction.add(R.id.dialpad_fragment_container, dialpadFragment, DIALPAD_FRAGMENT_TAG);
searchFragment.setQuery("", CallInitiationType.Type.DIALPAD);
} else {
DialpadFragment dialpadFragment = getDialpadFragment();
dialpadFragment.setStartedFromNewIntent(fromNewIntent);
transaction.show(dialpadFragment);
searchFragment.setQuery(dialpadFragment.getQuery(), CallInitiationType.Type.DIALPAD);
}
transaction.commit();
notifyListenersOnSearchOpen();
}
/**
* Hides the dialpad, reveals the FAB and slides the toolbar back onto the screen.
*
* This method intentionally "hides" and does not "remove" the dialpad in order to preserve its
* state (i.e. we call {@link FragmentTransaction#hide(Fragment)} instead of {@link
* FragmentTransaction#remove(Fragment)}.
*
* @see {@link #closeSearch(boolean)} to "remove" the dialpad.
*/
private void hideDialpad(boolean animate) {
LogUtil.enterBlock("MainSearchController.hideDialpad");
Assert.checkArgument(isDialpadVisible());
fab.show();
toolbar.slideDown(animate, fragmentContainer);
toolbar.transferQueryFromDialpad(getDialpadFragment().getQuery());
activity.setTitle(R.string.main_activity_label);
DialpadFragment dialpadFragment = getDialpadFragment();
dialpadFragment.setAnimate(animate);
dialpadFragment.slideDown(
animate,
new AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
if (!(activity.isFinishing() || activity.isDestroyed())) {
activity.getFragmentManager().beginTransaction().hide(dialpadFragment).commit();
}
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
}
private void hideBottomNav() {
bottomNav.setVisibility(View.GONE);
}
private void showBottomNav() {
bottomNav.setVisibility(View.VISIBLE);
}
/** Should be called when {@link DialpadListener#onDialpadShown()} is called. */
public void onDialpadShown() {
LogUtil.enterBlock("MainSearchController.onDialpadShown");
getDialpadFragment().slideUp(true);
hideBottomNav();
}
/**
* @see SearchFragmentListener#onSearchListTouch()
*
There are 4 scenarios we support to provide a nice UX experience:
*
* - When the dialpad is visible with an empty query, close the search UI.
*
- When the dialpad is visible with a non-empty query, hide the dialpad.
*
- When the regular search UI is visible with an empty query, close the search UI.
*
- When the regular search UI is visible with a non-empty query, hide the keyboard.
*
*/
public void onSearchListTouch() {
LogUtil.enterBlock("MainSearchController.onSearchListTouched");
if (isDialpadVisible()) {
if (TextUtils.isEmpty(getDialpadFragment().getQuery())) {
Logger.get(activity)
.logImpression(
DialerImpression.Type.MAIN_TOUCH_DIALPAD_SEARCH_LIST_TO_CLOSE_SEARCH_AND_DIALPAD);
closeSearch(true);
} else {
Logger.get(activity)
.logImpression(DialerImpression.Type.MAIN_TOUCH_DIALPAD_SEARCH_LIST_TO_HIDE_DIALPAD);
hideDialpad(/* animate=*/ true);
}
} else if (isSearchVisible()) {
if (TextUtils.isEmpty(toolbar.getQuery())) {
Logger.get(activity)
.logImpression(DialerImpression.Type.MAIN_TOUCH_SEARCH_LIST_TO_CLOSE_SEARCH);
closeSearch(true);
} else {
Logger.get(activity)
.logImpression(DialerImpression.Type.MAIN_TOUCH_SEARCH_LIST_TO_HIDE_KEYBOARD);
closeKeyboard();
}
}
}
/**
* Should be called when the user presses the back button.
*
* @return true if #onBackPressed() handled to action.
*/
public boolean onBackPressed() {
if (isDialpadVisible() && !TextUtils.isEmpty(getDialpadFragment().getQuery())) {
LogUtil.i("MainSearchController.onBackPressed", "Dialpad visible with query");
Logger.get(activity)
.logImpression(DialerImpression.Type.MAIN_PRESS_BACK_BUTTON_TO_HIDE_DIALPAD);
hideDialpad(/* animate=*/ true);
return true;
} else if (isSearchVisible()) {
LogUtil.i("MainSearchController.onBackPressed", "Search is visible");
Logger.get(activity)
.logImpression(
isDialpadVisible()
? DialerImpression.Type.MAIN_PRESS_BACK_BUTTON_TO_CLOSE_SEARCH_AND_DIALPAD
: DialerImpression.Type.MAIN_PRESS_BACK_BUTTON_TO_CLOSE_SEARCH);
closeSearch(true);
return true;
} else {
return false;
}
}
/** Calls {@link #hideDialpad(boolean)}, removes the search fragment and clears the dialpad. */
private void closeSearch(boolean animate) {
LogUtil.enterBlock("MainSearchController.closeSearch");
Assert.checkArgument(isSearchVisible());
if (isDialpadVisible()) {
hideDialpad(animate);
} else if (!fab.isShown()) {
fab.show();
}
showBottomNav();
toolbar.collapse(animate);
toolbarShadow.setVisibility(View.GONE);
activity.getFragmentManager().beginTransaction().hide(getSearchFragment()).commit();
// Clear the dialpad so the phone number isn't persisted between search sessions.
DialpadFragment dialpadFragment = getDialpadFragment();
if (dialpadFragment != null) {
// Temporarily disable accessibility when we clear the dialpad, since it should be
// invisible and should not announce anything.
dialpadFragment
.getDigitsWidget()
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
dialpadFragment.clearDialpad();
dialpadFragment
.getDigitsWidget()
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
}
notifyListenersOnSearchClose();
}
@Nullable
protected DialpadFragment getDialpadFragment() {
return (DialpadFragment) activity.getFragmentManager().findFragmentByTag(DIALPAD_FRAGMENT_TAG);
}
@Nullable
private NewSearchFragment getSearchFragment() {
return (NewSearchFragment) activity.getFragmentManager().findFragmentByTag(SEARCH_FRAGMENT_TAG);
}
private boolean isDialpadVisible() {
DialpadFragment fragment = getDialpadFragment();
return fragment != null
&& fragment.isAdded()
&& !fragment.isHidden()
&& fragment.isDialpadSlideUp();
}
private boolean isSearchVisible() {
NewSearchFragment fragment = getSearchFragment();
return fragment != null && fragment.isAdded() && !fragment.isHidden();
}
/** Returns true if the search UI is visible. */
public boolean isInSearch() {
return isSearchVisible();
}
/** Closes the keyboard if necessary. */
private void closeKeyboard() {
NewSearchFragment fragment = getSearchFragment();
if (fragment != null && fragment.isAdded()) {
toolbar.hideKeyboard();
}
}
/**
* Opens search in regular/search bar search mode.
*
* Hides fab, expands toolbar and starts the search fragment.
*/
@Override
public void onSearchBarClicked() {
LogUtil.enterBlock("MainSearchController.onSearchBarClicked");
Logger.get(activity).logImpression(DialerImpression.Type.MAIN_CLICK_SEARCH_BAR);
openSearch(Optional.absent());
}
private void openSearch(Optional query) {
LogUtil.enterBlock("MainSearchController.openSearch");
Logger.get(activity).logScreenView(ScreenEvent.Type.MAIN_SEARCH, activity);
fab.hide();
toolbar.expand(/* animate=*/ true, query);
toolbar.showKeyboard();
toolbarShadow.setVisibility(View.VISIBLE);
hideBottomNav();
FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
NewSearchFragment searchFragment = getSearchFragment();
// Show Search
if (searchFragment == null) {
searchFragment = NewSearchFragment.newInstance();
transaction.add(R.id.search_fragment_container, searchFragment, SEARCH_FRAGMENT_TAG);
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
} else if (!isSearchVisible()) {
transaction.show(getSearchFragment());
}
searchFragment.setQuery(
query.isPresent() ? query.get() : "", CallInitiationType.Type.REGULAR_SEARCH);
transaction.commit();
notifyListenersOnSearchOpen();
}
@Override
public void onSearchBackButtonClicked() {
LogUtil.enterBlock("MainSearchController.onSearchBackButtonClicked");
closeSearch(true);
}
@Override
public void onSearchQueryUpdated(String query) {
NewSearchFragment fragment = getSearchFragment();
if (fragment != null) {
fragment.setQuery(query, CallInitiationType.Type.REGULAR_SEARCH);
}
}
/** @see OnDialpadQueryChangedListener#onDialpadQueryChanged(java.lang.String) */
public void onDialpadQueryChanged(String query) {
query = SmartDialNameMatcher.normalizeNumber(/* context = */ activity, query);
NewSearchFragment fragment = getSearchFragment();
if (fragment != null) {
fragment.setQuery(query, CallInitiationType.Type.DIALPAD);
}
getDialpadFragment().process_quote_emergency_unquote(query);
}
@Override
public void onVoiceButtonClicked(VoiceSearchResultCallback voiceSearchResultCallback) {
Logger.get(activity).logImpression(DialerImpression.Type.MAIN_CLICK_SEARCH_BAR_VOICE_BUTTON);
try {
Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
activity.startActivityForResult(voiceIntent, ActivityRequestCodes.DIALTACTS_VOICE_SEARCH);
} catch (ActivityNotFoundException e) {
Toast.makeText(activity, R.string.voice_search_not_available, Toast.LENGTH_SHORT).show();
}
}
@Override
public boolean onMenuItemClicked(MenuItem menuItem) {
if (menuItem.getItemId() == R.id.settings) {
activity.startActivity(new Intent(activity, DialerSettingsActivity.class));
Logger.get(activity).logScreenView(ScreenEvent.Type.SETTINGS, activity);
return true;
} else if (menuItem.getItemId() == R.id.clear_frequents) {
ClearFrequentsDialog.show(activity.getFragmentManager());
Logger.get(activity).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, activity);
return true;
} else if (menuItem.getItemId() == R.id.menu_call_history) {
final Intent intent = new Intent(activity, CallLogActivity.class);
activity.startActivity(intent);
}
return false;
}
@Override
public void onActivityPause() {
LogUtil.enterBlock("MainSearchController.onActivityPause");
closeKeyboard();
if (closeSearchOnPause) {
closeSearchOnPause = false;
if (isInSearch()) {
closeSearch(false);
}
}
}
@Override
public void onUserLeaveHint() {
if (isInSearch()) {
// Requesting a permission causes this to be called and we want search to remain open when
// that happens. Otherwise, close search.
closeSearchOnPause = !requestingPermission;
// Always hide the keyboard when the user leaves dialer (including permission requests)
closeKeyboard();
}
}
@Override
public void onCallPlacedFromSearch() {
closeSearchOnPause = true;
}
@Override
public void requestingPermission() {
LogUtil.enterBlock("MainSearchController.requestingPermission");
requestingPermission = true;
}
public void onVoiceResults(int resultCode, Intent data) {
if (resultCode == AppCompatActivity.RESULT_OK) {
ArrayList matches = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
if (matches.size() > 0) {
LogUtil.i("MainSearchController.onVoiceResults", "voice search - match found");
openSearch(Optional.of(matches.get(0)));
} else {
LogUtil.i("MainSearchController.onVoiceResults", "voice search - nothing heard");
}
} else {
LogUtil.e("MainSearchController.onVoiceResults", "voice search failed");
}
}
public void onSaveInstanceState(Bundle bundle) {
bundle.putBoolean(KEY_IS_FAB_HIDDEN, !fab.isShown());
bundle.putInt(KEY_TOOLBAR_SHADOW_VISIBILITY, toolbarShadow.getVisibility());
bundle.putBoolean(KEY_IS_TOOLBAR_EXPANDED, toolbar.isExpanded());
bundle.putBoolean(KEY_IS_TOOLBAR_SLIDE_UP, toolbar.isSlideUp());
}
public void onRestoreInstanceState(Bundle savedInstanceState) {
toolbarShadow.setVisibility(savedInstanceState.getInt(KEY_TOOLBAR_SHADOW_VISIBILITY));
if (savedInstanceState.getBoolean(KEY_IS_FAB_HIDDEN, false)) {
fab.hide();
}
if (savedInstanceState.getBoolean(KEY_IS_TOOLBAR_EXPANDED, false)) {
toolbar.expand(false, Optional.absent());
}
if (savedInstanceState.getBoolean(KEY_IS_TOOLBAR_SLIDE_UP, false)) {
toolbar.slideUp(false, fragmentContainer);
}
}
public void addOnSearchShowListener(OnSearchShowListener listener) {
onSearchShowListenerList.add(listener);
}
public void removeOnSearchShowListener(OnSearchShowListener listener) {
onSearchShowListenerList.remove(listener);
}
private void notifyListenersOnSearchOpen() {
for (OnSearchShowListener listener : onSearchShowListenerList) {
listener.onSearchOpen();
}
}
private void notifyListenersOnSearchClose() {
for (OnSearchShowListener listener : onSearchShowListenerList) {
listener.onSearchClose();
}
}
/** Listener for search fragment show states change */
public interface OnSearchShowListener {
void onSearchOpen();
void onSearchClose();
}
}