package org.wikipedia.search;
import android.content.DialogInterface;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.wikipedia.BackPressedHandler;
import org.wikipedia.R;
import org.wikipedia.WikipediaApp;
import org.wikipedia.activity.FragmentUtil;
import org.wikipedia.analytics.SearchFunnel;
import org.wikipedia.concurrency.SaneAsyncTask;
import org.wikipedia.database.contract.SearchHistoryContract;
import org.wikipedia.history.HistoryEntry;
import org.wikipedia.page.PageTitle;
import org.wikipedia.readinglist.AddToReadingListDialog;
import org.wikipedia.settings.LanguagePreferenceDialog;
import org.wikipedia.settings.Prefs;
import org.wikipedia.util.DeviceUtil;
import org.wikipedia.util.FeedbackUtil;
import org.wikipedia.views.ViewUtil;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
import static org.apache.commons.lang3.StringUtils.isBlank;
public class SearchFragment extends Fragment implements BackPressedHandler,
SearchResultsFragment.Callback, RecentSearchesFragment.Parent {
public interface Callback {
void onSearchSelectPage(@NonNull HistoryEntry entry, boolean inNewTab);
void onSearchOpen();
void onSearchClose(boolean launchedFromIntent);
void onSearchResultCopyLink(@NonNull PageTitle title);
void onSearchResultAddToList(@NonNull PageTitle title,
@NonNull AddToReadingListDialog.InvokeSource source);
void onSearchResultShareLink(@NonNull PageTitle title);
}
private static final String ARG_INVOKE_SOURCE = "invokeSource";
private static final String ARG_QUERY = "lastQuery";
private static final String ARG_STATUS_BAR_VISIBLE = "statusBarVisible";
private static final int PANEL_RECENT_SEARCHES = 0;
private static final int PANEL_SEARCH_RESULTS = 1;
@BindView(R.id.empty_status_bar) View statusBarView;
@BindView(R.id.search_container) View searchContainer;
@BindView(R.id.search_toolbar) Toolbar toolbar;
@BindView(R.id.search_cab_view) SearchView searchView;
@BindView(R.id.search_progress_bar) ProgressBar progressBar;
@BindView(R.id.search_lang_button_container) View langButtonContainer;
@BindView(R.id.search_lang_button) TextView langButton;
private Unbinder unbinder;
private WikipediaApp app;
@BindView(android.support.v7.appcompat.R.id.search_src_text) EditText searchEditText;
private SearchFunnel funnel;
private SearchInvokeSource invokeSource;
/**
* Whether the Search fragment is currently showing.
*/
private boolean isSearchActive;
/**
* The last search term that the user entered. This will be passed into
* the TitleSearch and FullSearch sub-fragments.
*/
@Nullable private String query;
private boolean statusBarVisible;
private RecentSearchesFragment recentSearchesFragment;
private SearchResultsFragment searchResultsFragment;
private final SearchView.OnCloseListener searchCloseListener = new SearchView.OnCloseListener() {
@Override
public boolean onClose() {
closeSearch();
funnel.searchCancel();
return false;
}
};
private final SearchView.OnQueryTextListener searchQueryListener = new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String queryText) {
PageTitle firstResult = null;
if (getActivePanel() == PANEL_SEARCH_RESULTS) {
firstResult = searchResultsFragment.getFirstResult();
}
if (firstResult != null) {
navigateToTitle(firstResult, false, 0);
}
return true;
}
@Override
public boolean onQueryTextChange(String queryText) {
startSearch(queryText.trim(), false);
return true;
}
};
@NonNull public static SearchFragment newInstance(@NonNull SearchInvokeSource source,
@Nullable String query,
boolean statusBarVisible) {
SearchFragment fragment = new SearchFragment();
Bundle args = new Bundle();
args.putInt(ARG_INVOKE_SOURCE, source.code());
args.putString(ARG_QUERY, query);
args.putBoolean(ARG_STATUS_BAR_VISIBLE, statusBarVisible);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = WikipediaApp.getInstance();
invokeSource = SearchInvokeSource.of(getArguments().getInt(ARG_INVOKE_SOURCE));
query = getArguments().getString(ARG_QUERY);
statusBarVisible = getArguments().getBoolean(ARG_STATUS_BAR_VISIBLE);
funnel = new SearchFunnel(app, invokeSource);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
app = WikipediaApp.getInstance();
View view = inflater.inflate(R.layout.fragment_search, container, false);
unbinder = ButterKnife.bind(this, view);
statusBarView.setVisibility(statusBarVisible ? View.VISIBLE : View.GONE);
FragmentManager childFragmentManager = getChildFragmentManager();
recentSearchesFragment = (RecentSearchesFragment)childFragmentManager.findFragmentById(
R.id.search_panel_recent);
searchResultsFragment = (SearchResultsFragment)childFragmentManager.findFragmentById(
R.id.fragment_search_results);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onBackPressed();
}
});
initSearchView();
initLangButton();
if (!TextUtils.isEmpty(query)) {
showPanel(PANEL_SEARCH_RESULTS);
}
startSearch(query, false);
return view;
}
@Override
public void onDestroyView() {
searchView.setOnCloseListener(null);
searchView.setOnQueryTextListener(null);
unbinder.unbind();
unbinder = null;
super.onDestroyView();
}
@Override
@NonNull
public SearchFunnel getFunnel() {
return funnel;
}
public boolean isLaunchedFromIntent() {
return invokeSource.fromIntent();
}
@Override
public void switchToSearch(@NonNull String queryText) {
startSearch(queryText, true);
searchView.setQuery(queryText, false);
}
/**
* Changes the search text box to contain a different string.
* @param text The text you want to make the search box display.
*/
@Override
public void setSearchText(@NonNull CharSequence text) {
searchView.setQuery(text, false);
}
/**
* Determine whether the Search fragment is currently active.
* @return Whether the Search fragment is active.
*/
public boolean isSearchActive() {
return isSearchActive;
}
@Override
public boolean onBackPressed() {
if (isSearchActive) {
// todo: activity or fragment transition
closeSearch();
funnel.searchCancel();
return true;
}
return false;
}
@Override
public void navigateToTitle(@NonNull PageTitle title, boolean inNewTab, int position) {
if (!isAdded()) {
return;
}
funnel.searchClick(position);
HistoryEntry historyEntry = new HistoryEntry(title, HistoryEntry.SOURCE_SEARCH);
Callback callback = callback();
if (callback != null) {
callback.onSearchSelectPage(historyEntry, inNewTab);
}
closeSearch();
}
@Override
public void onSearchResultCopyLink(@NonNull PageTitle title) {
Callback callback = callback();
if (callback != null) {
callback.onSearchResultCopyLink(title);
}
}
@Override
public void onSearchResultAddToList(@NonNull PageTitle title,
@NonNull AddToReadingListDialog.InvokeSource source) {
Callback callback = callback();
if (callback != null) {
callback.onSearchResultAddToList(title, source);
}
}
@Override
public void onSearchResultShareLink(@NonNull PageTitle title) {
Callback callback = callback();
if (callback != null) {
callback.onSearchResultShareLink(title);
}
}
@Override
public void onSearchProgressBar(boolean enabled) {
progressBar.setVisibility(enabled ? View.VISIBLE : View.GONE);
}
@OnClick(R.id.search_container) void onSearchContainerClick() {
// Give the root container view an empty click handler, so that click events won't
// get passed down to any underlying views (e.g. a PageFragment on top of which
// this fragment is shown)
}
@OnClick(R.id.search_lang_button_container) void onLangButtonClick() {
showLangPreferenceDialog();
}
/**
* Kick off a search, based on a given search term. Will automatically pass the search to
* Title search or Full search, based on which one is currently displayed.
* If the search term is empty, the "recent searches" view will be shown.
* @param term Phrase to search for.
* @param force Whether to "force" starting this search. If the search is not forced, the
* search may be delayed by a small time, so that network requests are not sent
* too often. If the search is forced, the network request is sent immediately.
*/
private void startSearch(@Nullable String term, boolean force) {
if (!isSearchActive) {
openSearch();
}
if (TextUtils.isEmpty(term)) {
showPanel(PANEL_RECENT_SEARCHES);
} else if (getActivePanel() == PANEL_RECENT_SEARCHES) {
//start with title search...
showPanel(PANEL_SEARCH_RESULTS);
}
query = term;
if (isBlank(term) && !force) {
return;
}
searchResultsFragment.startSearch(term, force);
}
/**
* Activate the Search fragment.
*/
private void openSearch() {
// create a new funnel every time Search is opened, to get a new session ID
funnel = new SearchFunnel(WikipediaApp.getInstance(), invokeSource);
funnel.searchStart();
isSearchActive = true;
Callback callback = callback();
if (callback != null) {
callback.onSearchOpen();
}
// show ourselves
ViewUtil.fadeIn(searchContainer);
searchView.setIconified(false);
searchView.requestFocusFromTouch();
// if we already have a previous search query, then put it into the SearchView, and it will
// automatically trigger the showing of the corresponding search results.
if (isValidQuery(query)) {
searchView.setQuery(query, false);
searchEditText.selectAll();
}
}
public void closeSearch() {
isSearchActive = false;
// hide ourselves
ViewUtil.fadeOut(searchContainer);
DeviceUtil.hideSoftKeyboard(getView());
Callback callback = callback();
if (callback != null) {
callback.onSearchClose(invokeSource.fromIntent());
}
addRecentSearch(query);
}
/**
* Show a particular panel, which can be one of:
* - PANEL_RECENT_SEARCHES
* - PANEL_SEARCH_RESULTS
* Automatically hides the previous panel.
* @param panel Which panel to show.
*/
private void showPanel(int panel) {
switch (panel) {
case PANEL_RECENT_SEARCHES:
searchResultsFragment.hide();
recentSearchesFragment.show();
break;
case PANEL_SEARCH_RESULTS:
recentSearchesFragment.hide();
searchResultsFragment.show();
break;
default:
break;
}
}
private int getActivePanel() {
if (searchResultsFragment.isShowing()) {
return PANEL_SEARCH_RESULTS;
} else {
//otherwise, the recent searches must be showing:
return PANEL_RECENT_SEARCHES;
}
}
private void initSearchView() {
searchView.setOnQueryTextListener(searchQueryListener);
searchView.setOnCloseListener(searchCloseListener);
// reset its background
searchEditText.setBackgroundColor(Color.TRANSPARENT);
// make the search frame match_parent
View searchEditFrame = searchView
.findViewById(android.support.v7.appcompat.R.id.search_edit_frame);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
searchEditFrame.setLayoutParams(params);
// center the search text in it
searchEditText.setGravity(Gravity.CENTER_VERTICAL);
// remove focus line from search plate
View searchEditPlate = searchView
.findViewById(android.support.v7.appcompat.R.id.search_plate);
searchEditPlate.setBackgroundColor(Color.TRANSPARENT);
ImageView searchClose = (ImageView) searchView.findViewById(
android.support.v7.appcompat.R.id.search_close_btn);
FeedbackUtil.setToolbarButtonLongPressToast(searchClose);
}
private void initLangButton() {
if (!Prefs.getMediaWikiBaseUriSupportsLangCode()) {
langButtonContainer.setVisibility(View.GONE);
return;
}
langButton.setText(app.getAppOrSystemLanguageCode().toUpperCase());
formatLangButtonText();
FeedbackUtil.setToolbarButtonLongPressToast(langButtonContainer);
}
private boolean isValidQuery(String queryText) {
return queryText != null && TextUtils.getTrimmedLength(queryText) > 0;
}
private void addRecentSearch(String title) {
if (isValidQuery(title)) {
new SaveRecentSearchTask(new RecentSearch(title)).execute();
}
}
private final class SaveRecentSearchTask extends SaneAsyncTask<Void> {
private final RecentSearch entry;
SaveRecentSearchTask(RecentSearch entry) {
this.entry = entry;
}
@Override
public Void performTask() throws Throwable {
app.getDatabaseClient(RecentSearch.class).upsert(entry, SearchHistoryContract.Query.SELECTION);
return null;
}
@Override
public void onFinish(Void result) {
super.onFinish(result);
recentSearchesFragment.updateList();
}
@Override
public void onCatch(Throwable caught) {
Log.w("SaveRecentSearchTask", "Caught " + caught.getMessage(), caught);
}
}
private void formatLangButtonText() {
final int langCodeStandardLength = 3;
final int langButtonTextMaxLength = 7;
// These values represent scaled pixels (sp)
final int langButtonTextSizeSmaller = 10;
final int langButtonTextSizeLarger = 13;
String langCode = app.getAppOrSystemLanguageCode();
if (langCode.length() > langCodeStandardLength) {
langButton.setTextSize(langButtonTextSizeSmaller);
if (langCode.length() > langButtonTextMaxLength) {
langButton.setText(langCode.substring(0, langButtonTextMaxLength).toUpperCase());
}
return;
}
langButton.setTextSize(langButtonTextSizeLarger);
}
@Nullable
private Callback callback() {
return FragmentUtil.getCallback(this, Callback.class);
}
private void showLangPreferenceDialog() {
LanguagePreferenceDialog langPrefDialog = new LanguagePreferenceDialog(getContext(), true);
langPrefDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (getActivity() == null) {
return;
}
langButton.setText(app.getAppOrSystemLanguageCode().toUpperCase());
formatLangButtonText();
if (!TextUtils.isEmpty(query)) {
startSearch(query, true);
}
}
});
langPrefDialog.show();
}
}