diff options
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java')
-rw-r--r-- | mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java | 494 |
1 files changed, 0 insertions, 494 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java deleted file mode 100644 index 5b97a8f5f..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java +++ /dev/null @@ -1,494 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.home; - -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.R; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.home.BrowserSearch.OnEditSuggestionListener; -import org.mozilla.gecko.home.BrowserSearch.OnSearchListener; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.icons.IconResponse; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.util.DrawableUtil; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.widget.AnimatedHeightLayout; -import org.mozilla.gecko.widget.FaviconView; -import org.mozilla.gecko.widget.FlowLayout; - -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.drawable.Drawable; -import android.graphics.Typeface; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.text.style.StyleSpan; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.animation.AlphaAnimation; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.List; -import java.util.regex.Pattern; - -class SearchEngineRow extends AnimatedHeightLayout { - // Duration for fade-in animation - private static final int ANIMATION_DURATION = 250; - - // Inner views - private final FlowLayout mSuggestionView; - private final FaviconView mIconView; - private final LinearLayout mUserEnteredView; - private final TextView mUserEnteredTextView; - - // Inflater used when updating from suggestions - private final LayoutInflater mInflater; - - // Search engine associated with this view - private SearchEngine mSearchEngine; - - // Event listeners for suggestion views - private final OnClickListener mClickListener; - private final OnLongClickListener mLongClickListener; - - // On URL open listener - private OnUrlOpenListener mUrlOpenListener; - - // On search listener - private OnSearchListener mSearchListener; - - // On edit suggestion listener - private OnEditSuggestionListener mEditSuggestionListener; - - // Selected suggestion view - private int mSelectedView; - - // android:backgroundTint only works in Android 21 and higher so we can't do this statically in the xml - private Drawable mSearchHistorySuggestionIcon; - - // Maximums for suggestions - private int mMaxSavedSuggestions; - private int mMaxSearchSuggestions; - - private final List<Integer> mOccurrences = new ArrayList<Integer>(); - - public SearchEngineRow(Context context) { - this(context, null); - } - - public SearchEngineRow(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public SearchEngineRow(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - mClickListener = new OnClickListener() { - @Override - public void onClick(View v) { - final String suggestion = getSuggestionTextFromView(v); - - // If we're not clicking the user-entered view (the first suggestion item) - // and the search matches a URL pattern, go to that URL. Otherwise, do a - // search for the term. - if (v != mUserEnteredView && !StringUtils.isSearchQuery(suggestion, true)) { - if (mUrlOpenListener != null) { - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "url"); - - mUrlOpenListener.onUrlOpen(suggestion, EnumSet.noneOf(OnUrlOpenListener.Flags.class)); - } - } else if (mSearchListener != null) { - if (v == mUserEnteredView) { - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user"); - } else { - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, (String) v.getTag()); - } - mSearchListener.onSearch(mSearchEngine, suggestion, TelemetryContract.Method.SUGGESTION); - } - } - }; - - mLongClickListener = new OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - if (mEditSuggestionListener != null) { - final String suggestion = getSuggestionTextFromView(v); - mEditSuggestionListener.onEditSuggestion(suggestion); - return true; - } - - return false; - } - }; - - mInflater = LayoutInflater.from(context); - mInflater.inflate(R.layout.search_engine_row, this); - - mSuggestionView = (FlowLayout) findViewById(R.id.suggestion_layout); - mIconView = (FaviconView) findViewById(R.id.suggestion_icon); - - // User-entered search term is first suggestion - mUserEnteredView = (LinearLayout) findViewById(R.id.suggestion_user_entered); - mUserEnteredView.setOnClickListener(mClickListener); - - mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text); - mSearchHistorySuggestionIcon = DrawableUtil.tintDrawableWithColorRes(getContext(), R.drawable.icon_most_recent_empty, R.color.tabs_tray_icon_grey); - - // Suggestion limits - mMaxSavedSuggestions = getResources().getInteger(R.integer.max_saved_suggestions); - mMaxSearchSuggestions = getResources().getInteger(R.integer.max_search_suggestions); - } - - private void setDescriptionOnSuggestion(View v, String suggestion) { - v.setContentDescription(getResources().getString(R.string.suggestion_for_engine, - mSearchEngine.name, suggestion)); - } - - private String getSuggestionTextFromView(View v) { - final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text); - return suggestionText.getText().toString(); - } - - /** - * Finds all occurrences of pattern in string and returns a list of the starting indices - * of each occurrence. - * - * @param pattern The pattern that is searched for - * @param string The string where we search for the pattern - */ - private void refreshOccurrencesWith(String pattern, String string) { - mOccurrences.clear(); - - // Don't try to search for an empty string - String.indexOf will return 0, which would result - // in us iterating with lastIndexOfMatch = 0, which eventually results in an OOM. - if (TextUtils.isEmpty(pattern)) { - return; - } - - final int patternLength = pattern.length(); - - int indexOfMatch = 0; - int lastIndexOfMatch = 0; - while (indexOfMatch != -1) { - indexOfMatch = string.indexOf(pattern, lastIndexOfMatch); - lastIndexOfMatch = indexOfMatch + patternLength; - if (indexOfMatch != -1) { - mOccurrences.add(indexOfMatch); - } - } - } - - /** - * Sets the content for the suggestion view. - * - * If the suggestion doesn't contain mUserSearchTerm, nothing is made bold. - * All instances of mUserSearchTerm in the suggestion are not bold. - * - * @param v The View that needs to be populated - * @param suggestion The suggestion text that will be placed in the view - * @param isUserSavedSearch whether the suggestion is from history or not - */ - private void setSuggestionOnView(View v, String suggestion, boolean isUserSavedSearch) { - final ImageView historyIcon = (ImageView) v.findViewById(R.id.suggestion_item_icon); - if (isUserSavedSearch) { - historyIcon.setImageDrawable(mSearchHistorySuggestionIcon); - historyIcon.setVisibility(View.VISIBLE); - } else { - historyIcon.setVisibility(View.GONE); - } - - final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text); - final String searchTerm = getSuggestionTextFromView(mUserEnteredView); - final int searchTermLength = searchTerm.length(); - refreshOccurrencesWith(searchTerm, suggestion); - if (mOccurrences.size() > 0) { - final SpannableStringBuilder sb = new SpannableStringBuilder(suggestion); - int nextStartSpanIndex = 0; - // Done to make sure that the stretch of text after the last occurrence, till the end of the suggestion, is made bold - mOccurrences.add(suggestion.length()); - for (int occurrence : mOccurrences) { - // Even though they're the same style, SpannableStringBuilder will interpret there as being only one Span present if we re-use a StyleSpan - StyleSpan boldSpan = new StyleSpan(Typeface.BOLD); - sb.setSpan(boldSpan, nextStartSpanIndex, occurrence, Spannable.SPAN_INCLUSIVE_INCLUSIVE); - nextStartSpanIndex = occurrence + searchTermLength; - } - mOccurrences.clear(); - suggestionText.setText(sb); - } else { - suggestionText.setText(suggestion); - } - - setDescriptionOnSuggestion(suggestionText, suggestion); - } - - /** - * Perform a search for the user-entered term. - */ - public void performUserEnteredSearch() { - String searchTerm = getSuggestionTextFromView(mUserEnteredView); - if (mSearchListener != null) { - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user"); - mSearchListener.onSearch(mSearchEngine, searchTerm, TelemetryContract.Method.SUGGESTION); - } - } - - public void setSearchTerm(String searchTerm) { - mUserEnteredTextView.setText(searchTerm); - - // mSearchEngine is not set in the first call to this method; the content description - // is instead initially set in updateSuggestions(). - if (mSearchEngine != null) { - setDescriptionOnSuggestion(mUserEnteredTextView, searchTerm); - } - } - - public void setOnUrlOpenListener(OnUrlOpenListener listener) { - mUrlOpenListener = listener; - } - - public void setOnSearchListener(OnSearchListener listener) { - mSearchListener = listener; - } - - public void setOnEditSuggestionListener(OnEditSuggestionListener listener) { - mEditSuggestionListener = listener; - } - - private void bindSuggestionView(String suggestion, boolean animate, int recycledSuggestionCount, Integer previousSuggestionChildIndex, boolean isUserSavedSearch, String telemetryTag) { - final View suggestionItem; - - // Reuse suggestion views from recycled view, if possible. - if (previousSuggestionChildIndex + 1 < recycledSuggestionCount) { - suggestionItem = mSuggestionView.getChildAt(previousSuggestionChildIndex + 1); - suggestionItem.setVisibility(View.VISIBLE); - } else { - suggestionItem = mInflater.inflate(R.layout.suggestion_item, null); - - suggestionItem.setOnClickListener(mClickListener); - suggestionItem.setOnLongClickListener(mLongClickListener); - - suggestionItem.setTag(telemetryTag); - - mSuggestionView.addView(suggestionItem); - } - - setSuggestionOnView(suggestionItem, suggestion, isUserSavedSearch); - - if (animate) { - AlphaAnimation anim = new AlphaAnimation(0, 1); - anim.setDuration(ANIMATION_DURATION); - anim.setStartOffset(previousSuggestionChildIndex * ANIMATION_DURATION); - suggestionItem.startAnimation(anim); - } - } - - private void hideRecycledSuggestions(int lastVisibleChildIndex, int recycledSuggestionCount) { - // Hide extra suggestions that have been recycled. - for (int i = lastVisibleChildIndex + 1; i < recycledSuggestionCount; ++i) { - mSuggestionView.getChildAt(i).setVisibility(View.GONE); - } - } - - /** - * Displays search suggestions from previous searches. - * - * @param savedSuggestions The List to iterate over for saved search suggestions to display. This function does not - * enforce a ui maximum or filter. It will show all the suggestions in this list. - * @param suggestionStartIndex global index of where to start adding suggestion "buttons" in the search engine row. Also - * acts as a counter for total number of suggestions visible. - * @param animate whether or not to animate suggestions for visual polish - * @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls - */ - private void updateFromSavedSearches(List<String> savedSuggestions, boolean animate, int suggestionStartIndex, int recycledSuggestionCount) { - if (savedSuggestions == null || savedSuggestions.isEmpty()) { - hideRecycledSuggestions(suggestionStartIndex, recycledSuggestionCount); - return; - } - - final int numSavedSearches = savedSuggestions.size(); - int indexOfPreviousSuggestion = 0; - for (int i = 0; i < numSavedSearches; i++) { - String telemetryTag = "history." + i; - final String suggestion = savedSuggestions.get(i); - indexOfPreviousSuggestion = suggestionStartIndex + i; - bindSuggestionView(suggestion, animate, recycledSuggestionCount, indexOfPreviousSuggestion, true, telemetryTag); - } - - hideRecycledSuggestions(indexOfPreviousSuggestion + 1, recycledSuggestionCount); - } - - /** - * Displays suggestions supplied by the search engine, relative to number of suggestions from search history. - * - * @param animate whether or not to animate suggestions for visual polish - * @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls - * @param savedSuggestionCount how many saved searches this searchTerm has - * @return the global count of how many suggestions have been bound/shown in the search engine row - */ - private int updateFromSearchEngine(boolean animate, List<String> searchEngineSuggestions, int recycledSuggestionCount, int savedSuggestionCount) { - int maxSuggestions = mMaxSearchSuggestions; - // If there are less than max saved searches on phones, fill the space with more search engine suggestions - if (!HardwareUtils.isTablet() && savedSuggestionCount < mMaxSavedSuggestions) { - maxSuggestions += mMaxSavedSuggestions - savedSuggestionCount; - } - - final int numSearchEngineSuggestions = searchEngineSuggestions.size(); - int relativeIndex; - for (relativeIndex = 0; relativeIndex < numSearchEngineSuggestions; relativeIndex++) { - if (relativeIndex == maxSuggestions) { - break; - } - - // Since the search engine suggestions are listed first, their relative index is their global index - String telemetryTag = "engine." + relativeIndex; - final String suggestion = searchEngineSuggestions.get(relativeIndex); - bindSuggestionView(suggestion, animate, recycledSuggestionCount, relativeIndex, false, telemetryTag); - } - - hideRecycledSuggestions(relativeIndex + 1, recycledSuggestionCount); - - // Make sure mSelectedView is still valid. - if (mSelectedView >= mSuggestionView.getChildCount()) { - mSelectedView = mSuggestionView.getChildCount() - 1; - } - - return relativeIndex; - } - - /** - * Updates the whole suggestions UI, the search engine UI, suggestions from the default search engine, - * and suggestions from search history. - * - * This can be called before the opt-in permission prompt is shown or set. - * Even if both suggestion types are disabled, we need to update the search engine, its image, and the content description. - * - * @param searchSuggestionsEnabled whether or not suggestions from the default search engine are enabled - * @param searchEngine the search engine to use throughout the SearchEngineRow class - * @param rawSearchHistorySuggestions search history suggestions - * @param animate whether or not to use animations - **/ - public void updateSuggestions(boolean searchSuggestionsEnabled, SearchEngine searchEngine, @Nullable List<String> rawSearchHistorySuggestions, boolean animate) { - mSearchEngine = searchEngine; - // Set the search engine icon (e.g., Google) for the row. - - mIconView.updateAndScaleImage(IconResponse.create(mSearchEngine.getIcon())); - // Set the initial content description. - setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString()); - - final int recycledSuggestionCount = mSuggestionView.getChildCount(); - final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext()); - final boolean savedSearchesEnabled = prefs.getBoolean(GeckoPreferences.PREFS_HISTORY_SAVED_SEARCH, true); - - // Remove duplicates of search engine suggestions from saved searches. - List<String> searchHistorySuggestions = (rawSearchHistorySuggestions != null) ? rawSearchHistorySuggestions : new ArrayList<String>(); - - // Filter out URLs and long search suggestions - Iterator<String> searchHistoryIterator = searchHistorySuggestions.iterator(); - while (searchHistoryIterator.hasNext()) { - final String currentSearchHistory = searchHistoryIterator.next(); - - if (currentSearchHistory.length() > 50 || Pattern.matches("^(https?|ftp|file)://.*", currentSearchHistory)) { - searchHistoryIterator.remove(); - } - } - - - List<String> searchEngineSuggestions = new ArrayList<String>(); - for (String suggestion : searchEngine.getSuggestions()) { - searchHistorySuggestions.remove(suggestion); - searchEngineSuggestions.add(suggestion); - } - // Make sure the search term itself isn't duplicated. This is more important on phones than tablets where screen - // space is more precious. - searchHistorySuggestions.remove(getSuggestionTextFromView(mUserEnteredView)); - - // Trim the history suggestions down to the maximum allowed. - if (searchHistorySuggestions.size() >= mMaxSavedSuggestions) { - // The second index to subList() is exclusive, so this looks like an off by one error but it is not. - searchHistorySuggestions = searchHistorySuggestions.subList(0, mMaxSavedSuggestions); - } - final int searchHistoryCount = searchHistorySuggestions.size(); - - if (searchSuggestionsEnabled && savedSearchesEnabled) { - final int suggestionViewCount = updateFromSearchEngine(animate, searchEngineSuggestions, recycledSuggestionCount, searchHistoryCount); - updateFromSavedSearches(searchHistorySuggestions, animate, suggestionViewCount, recycledSuggestionCount); - } else if (savedSearchesEnabled) { - updateFromSavedSearches(searchHistorySuggestions, animate, 0, recycledSuggestionCount); - } else if (searchSuggestionsEnabled) { - updateFromSearchEngine(animate, searchEngineSuggestions, recycledSuggestionCount, 0); - } else { - // The current search term is treated separately from the suggestions list, hence we can - // recycle ALL suggestion items here. (We always show the current search term, i.e. 1 item, - // in front of the search engine suggestions and/or the search history.) - hideRecycledSuggestions(0, recycledSuggestionCount); - } - } - - @Override - public boolean onKeyDown(int keyCode, android.view.KeyEvent event) { - final View suggestion = mSuggestionView.getChildAt(mSelectedView); - - if (event.getAction() != android.view.KeyEvent.ACTION_DOWN) { - return false; - } - - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_DPAD_RIGHT: - final View nextSuggestion = mSuggestionView.getChildAt(mSelectedView + 1); - if (nextSuggestion != null) { - changeSelectedSuggestion(suggestion, nextSuggestion); - mSelectedView++; - return true; - } - break; - - case KeyEvent.KEYCODE_DPAD_LEFT: - final View prevSuggestion = mSuggestionView.getChildAt(mSelectedView - 1); - if (prevSuggestion != null) { - changeSelectedSuggestion(suggestion, prevSuggestion); - mSelectedView--; - return true; - } - break; - - case KeyEvent.KEYCODE_BUTTON_A: - // TODO: handle long pressing for editing suggestions - return suggestion.performClick(); - } - - return false; - } - - private void changeSelectedSuggestion(View oldSuggestion, View newSuggestion) { - oldSuggestion.setDuplicateParentStateEnabled(false); - newSuggestion.setDuplicateParentStateEnabled(true); - oldSuggestion.refreshDrawableState(); - newSuggestion.refreshDrawableState(); - } - - public void onSelected() { - mSelectedView = 0; - mUserEnteredView.setDuplicateParentStateEnabled(true); - mUserEnteredView.refreshDrawableState(); - } - - public void onDeselected() { - final View suggestion = mSuggestionView.getChildAt(mSelectedView); - suggestion.setDuplicateParentStateEnabled(false); - suggestion.refreshDrawableState(); - } -} |