diff -r 000000000000 -r 6474c204b198 mobile/android/base/home/SearchEngineRow.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/home/SearchEngineRow.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,284 @@ +/* -*- 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.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.util.StringUtils; +import org.mozilla.gecko.widget.AnimatedHeightLayout; +import org.mozilla.gecko.widget.FaviconView; +import org.mozilla.gecko.widget.FlowLayout; + +import android.content.Context; +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.EnumSet; + +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 = 0; + + 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, false)) { + 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, "engine"); + } + mSearchListener.onSearch(mSearchEngine, 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); + } + + 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(); + } + + private void setSuggestionOnView(View v, String suggestion) { + final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text); + 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); + } + } + + 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 updateFromSearchEngine. + 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; + } + + public void updateFromSearchEngine(SearchEngine searchEngine, boolean animate) { + // Update search engine reference. + mSearchEngine = searchEngine; + + // Set the search engine icon (e.g., Google) for the row. + mIconView.updateAndScaleImage(mSearchEngine.getIcon(), mSearchEngine.getEngineIdentifier()); + + // Set the initial content description. + setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString()); + + // Add additional suggestions given by this engine. + final int recycledSuggestionCount = mSuggestionView.getChildCount(); + + int suggestionCounter = 0; + for (String suggestion : mSearchEngine.getSuggestions()) { + final View suggestionItem; + + // Reuse suggestion views from recycled view, if possible. + if (suggestionCounter + 1 < recycledSuggestionCount) { + suggestionItem = mSuggestionView.getChildAt(suggestionCounter + 1); + suggestionItem.setVisibility(View.VISIBLE); + } else { + suggestionItem = mInflater.inflate(R.layout.suggestion_item, null); + + suggestionItem.setOnClickListener(mClickListener); + suggestionItem.setOnLongClickListener(mLongClickListener); + + final ImageView magnifier = + (ImageView) suggestionItem.findViewById(R.id.suggestion_magnifier); + magnifier.setVisibility(View.GONE); + + mSuggestionView.addView(suggestionItem); + } + + setSuggestionOnView(suggestionItem, suggestion); + + if (animate) { + AlphaAnimation anim = new AlphaAnimation(0, 1); + anim.setDuration(ANIMATION_DURATION); + anim.setStartOffset(suggestionCounter * ANIMATION_DURATION); + suggestionItem.startAnimation(anim); + } + + ++suggestionCounter; + } + + // Hide extra suggestions that have been recycled. + for (int i = suggestionCounter + 1; i < recycledSuggestionCount; ++i) { + mSuggestionView.getChildAt(i).setVisibility(View.GONE); + } + + // Make sure mSelectedView is still valid. + if (mSelectedView >= mSuggestionView.getChildCount()) { + mSelectedView = mSuggestionView.getChildCount() - 1; + } + } + + @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(); + } +}