1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/home/SearchEngineRow.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,284 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.home; 1.10 + 1.11 +import org.mozilla.gecko.R; 1.12 +import org.mozilla.gecko.Telemetry; 1.13 +import org.mozilla.gecko.TelemetryContract; 1.14 +import org.mozilla.gecko.home.BrowserSearch.OnEditSuggestionListener; 1.15 +import org.mozilla.gecko.home.BrowserSearch.OnSearchListener; 1.16 +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; 1.17 +import org.mozilla.gecko.util.StringUtils; 1.18 +import org.mozilla.gecko.widget.AnimatedHeightLayout; 1.19 +import org.mozilla.gecko.widget.FaviconView; 1.20 +import org.mozilla.gecko.widget.FlowLayout; 1.21 + 1.22 +import android.content.Context; 1.23 +import android.util.AttributeSet; 1.24 +import android.view.KeyEvent; 1.25 +import android.view.LayoutInflater; 1.26 +import android.view.View; 1.27 +import android.view.animation.AlphaAnimation; 1.28 +import android.widget.ImageView; 1.29 +import android.widget.LinearLayout; 1.30 +import android.widget.TextView; 1.31 + 1.32 +import java.util.EnumSet; 1.33 + 1.34 +class SearchEngineRow extends AnimatedHeightLayout { 1.35 + // Duration for fade-in animation 1.36 + private static final int ANIMATION_DURATION = 250; 1.37 + 1.38 + // Inner views 1.39 + private final FlowLayout mSuggestionView; 1.40 + private final FaviconView mIconView; 1.41 + private final LinearLayout mUserEnteredView; 1.42 + private final TextView mUserEnteredTextView; 1.43 + 1.44 + // Inflater used when updating from suggestions 1.45 + private final LayoutInflater mInflater; 1.46 + 1.47 + // Search engine associated with this view 1.48 + private SearchEngine mSearchEngine; 1.49 + 1.50 + // Event listeners for suggestion views 1.51 + private final OnClickListener mClickListener; 1.52 + private final OnLongClickListener mLongClickListener; 1.53 + 1.54 + // On URL open listener 1.55 + private OnUrlOpenListener mUrlOpenListener; 1.56 + 1.57 + // On search listener 1.58 + private OnSearchListener mSearchListener; 1.59 + 1.60 + // On edit suggestion listener 1.61 + private OnEditSuggestionListener mEditSuggestionListener; 1.62 + 1.63 + // Selected suggestion view 1.64 + private int mSelectedView = 0; 1.65 + 1.66 + public SearchEngineRow(Context context) { 1.67 + this(context, null); 1.68 + } 1.69 + 1.70 + public SearchEngineRow(Context context, AttributeSet attrs) { 1.71 + this(context, attrs, 0); 1.72 + } 1.73 + 1.74 + public SearchEngineRow(Context context, AttributeSet attrs, int defStyle) { 1.75 + super(context, attrs, defStyle); 1.76 + 1.77 + mClickListener = new OnClickListener() { 1.78 + @Override 1.79 + public void onClick(View v) { 1.80 + final String suggestion = getSuggestionTextFromView(v); 1.81 + 1.82 + // If we're not clicking the user-entered view (the first suggestion item) 1.83 + // and the search matches a URL pattern, go to that URL. Otherwise, do a 1.84 + // search for the term. 1.85 + if (v != mUserEnteredView && !StringUtils.isSearchQuery(suggestion, false)) { 1.86 + if (mUrlOpenListener != null) { 1.87 + Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "url"); 1.88 + 1.89 + mUrlOpenListener.onUrlOpen(suggestion, EnumSet.noneOf(OnUrlOpenListener.Flags.class)); 1.90 + } 1.91 + } else if (mSearchListener != null) { 1.92 + if (v == mUserEnteredView) { 1.93 + Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user"); 1.94 + } else { 1.95 + Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "engine"); 1.96 + } 1.97 + mSearchListener.onSearch(mSearchEngine, suggestion); 1.98 + } 1.99 + } 1.100 + }; 1.101 + 1.102 + mLongClickListener = new OnLongClickListener() { 1.103 + @Override 1.104 + public boolean onLongClick(View v) { 1.105 + if (mEditSuggestionListener != null) { 1.106 + final String suggestion = getSuggestionTextFromView(v); 1.107 + mEditSuggestionListener.onEditSuggestion(suggestion); 1.108 + return true; 1.109 + } 1.110 + 1.111 + return false; 1.112 + } 1.113 + }; 1.114 + 1.115 + mInflater = LayoutInflater.from(context); 1.116 + mInflater.inflate(R.layout.search_engine_row, this); 1.117 + 1.118 + mSuggestionView = (FlowLayout) findViewById(R.id.suggestion_layout); 1.119 + mIconView = (FaviconView) findViewById(R.id.suggestion_icon); 1.120 + 1.121 + // User-entered search term is first suggestion 1.122 + mUserEnteredView = (LinearLayout) findViewById(R.id.suggestion_user_entered); 1.123 + mUserEnteredView.setOnClickListener(mClickListener); 1.124 + 1.125 + mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text); 1.126 + } 1.127 + 1.128 + private void setDescriptionOnSuggestion(View v, String suggestion) { 1.129 + v.setContentDescription(getResources().getString(R.string.suggestion_for_engine, 1.130 + mSearchEngine.name, suggestion)); 1.131 + } 1.132 + 1.133 + private String getSuggestionTextFromView(View v) { 1.134 + final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text); 1.135 + return suggestionText.getText().toString(); 1.136 + } 1.137 + 1.138 + private void setSuggestionOnView(View v, String suggestion) { 1.139 + final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text); 1.140 + suggestionText.setText(suggestion); 1.141 + setDescriptionOnSuggestion(suggestionText, suggestion); 1.142 + } 1.143 + 1.144 + /** 1.145 + * Perform a search for the user-entered term. 1.146 + */ 1.147 + public void performUserEnteredSearch() { 1.148 + String searchTerm = getSuggestionTextFromView(mUserEnteredView); 1.149 + if (mSearchListener != null) { 1.150 + Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user"); 1.151 + mSearchListener.onSearch(mSearchEngine, searchTerm); 1.152 + } 1.153 + } 1.154 + 1.155 + public void setSearchTerm(String searchTerm) { 1.156 + mUserEnteredTextView.setText(searchTerm); 1.157 + 1.158 + // mSearchEngine is not set in the first call to this method; the content description 1.159 + // is instead initially set in updateFromSearchEngine. 1.160 + if (mSearchEngine != null) { 1.161 + setDescriptionOnSuggestion(mUserEnteredTextView, searchTerm); 1.162 + } 1.163 + } 1.164 + 1.165 + public void setOnUrlOpenListener(OnUrlOpenListener listener) { 1.166 + mUrlOpenListener = listener; 1.167 + } 1.168 + 1.169 + public void setOnSearchListener(OnSearchListener listener) { 1.170 + mSearchListener = listener; 1.171 + } 1.172 + 1.173 + public void setOnEditSuggestionListener(OnEditSuggestionListener listener) { 1.174 + mEditSuggestionListener = listener; 1.175 + } 1.176 + 1.177 + public void updateFromSearchEngine(SearchEngine searchEngine, boolean animate) { 1.178 + // Update search engine reference. 1.179 + mSearchEngine = searchEngine; 1.180 + 1.181 + // Set the search engine icon (e.g., Google) for the row. 1.182 + mIconView.updateAndScaleImage(mSearchEngine.getIcon(), mSearchEngine.getEngineIdentifier()); 1.183 + 1.184 + // Set the initial content description. 1.185 + setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString()); 1.186 + 1.187 + // Add additional suggestions given by this engine. 1.188 + final int recycledSuggestionCount = mSuggestionView.getChildCount(); 1.189 + 1.190 + int suggestionCounter = 0; 1.191 + for (String suggestion : mSearchEngine.getSuggestions()) { 1.192 + final View suggestionItem; 1.193 + 1.194 + // Reuse suggestion views from recycled view, if possible. 1.195 + if (suggestionCounter + 1 < recycledSuggestionCount) { 1.196 + suggestionItem = mSuggestionView.getChildAt(suggestionCounter + 1); 1.197 + suggestionItem.setVisibility(View.VISIBLE); 1.198 + } else { 1.199 + suggestionItem = mInflater.inflate(R.layout.suggestion_item, null); 1.200 + 1.201 + suggestionItem.setOnClickListener(mClickListener); 1.202 + suggestionItem.setOnLongClickListener(mLongClickListener); 1.203 + 1.204 + final ImageView magnifier = 1.205 + (ImageView) suggestionItem.findViewById(R.id.suggestion_magnifier); 1.206 + magnifier.setVisibility(View.GONE); 1.207 + 1.208 + mSuggestionView.addView(suggestionItem); 1.209 + } 1.210 + 1.211 + setSuggestionOnView(suggestionItem, suggestion); 1.212 + 1.213 + if (animate) { 1.214 + AlphaAnimation anim = new AlphaAnimation(0, 1); 1.215 + anim.setDuration(ANIMATION_DURATION); 1.216 + anim.setStartOffset(suggestionCounter * ANIMATION_DURATION); 1.217 + suggestionItem.startAnimation(anim); 1.218 + } 1.219 + 1.220 + ++suggestionCounter; 1.221 + } 1.222 + 1.223 + // Hide extra suggestions that have been recycled. 1.224 + for (int i = suggestionCounter + 1; i < recycledSuggestionCount; ++i) { 1.225 + mSuggestionView.getChildAt(i).setVisibility(View.GONE); 1.226 + } 1.227 + 1.228 + // Make sure mSelectedView is still valid. 1.229 + if (mSelectedView >= mSuggestionView.getChildCount()) { 1.230 + mSelectedView = mSuggestionView.getChildCount() - 1; 1.231 + } 1.232 + } 1.233 + 1.234 + @Override 1.235 + public boolean onKeyDown(int keyCode, android.view.KeyEvent event) { 1.236 + final View suggestion = mSuggestionView.getChildAt(mSelectedView); 1.237 + 1.238 + if (event.getAction() != android.view.KeyEvent.ACTION_DOWN) { 1.239 + return false; 1.240 + } 1.241 + 1.242 + switch (event.getKeyCode()) { 1.243 + case KeyEvent.KEYCODE_DPAD_RIGHT: 1.244 + final View nextSuggestion = mSuggestionView.getChildAt(mSelectedView + 1); 1.245 + if (nextSuggestion != null) { 1.246 + changeSelectedSuggestion(suggestion, nextSuggestion); 1.247 + mSelectedView++; 1.248 + return true; 1.249 + } 1.250 + break; 1.251 + 1.252 + case KeyEvent.KEYCODE_DPAD_LEFT: 1.253 + final View prevSuggestion = mSuggestionView.getChildAt(mSelectedView - 1); 1.254 + if (prevSuggestion != null) { 1.255 + changeSelectedSuggestion(suggestion, prevSuggestion); 1.256 + mSelectedView--; 1.257 + return true; 1.258 + } 1.259 + break; 1.260 + 1.261 + case KeyEvent.KEYCODE_BUTTON_A: 1.262 + // TODO: handle long pressing for editing suggestions 1.263 + return suggestion.performClick(); 1.264 + } 1.265 + 1.266 + return false; 1.267 + } 1.268 + 1.269 + private void changeSelectedSuggestion(View oldSuggestion, View newSuggestion) { 1.270 + oldSuggestion.setDuplicateParentStateEnabled(false); 1.271 + newSuggestion.setDuplicateParentStateEnabled(true); 1.272 + oldSuggestion.refreshDrawableState(); 1.273 + newSuggestion.refreshDrawableState(); 1.274 + } 1.275 + 1.276 + public void onSelected() { 1.277 + mSelectedView = 0; 1.278 + mUserEnteredView.setDuplicateParentStateEnabled(true); 1.279 + mUserEnteredView.refreshDrawableState(); 1.280 + } 1.281 + 1.282 + public void onDeselected() { 1.283 + final View suggestion = mSuggestionView.getChildAt(mSelectedView); 1.284 + suggestion.setDuplicateParentStateEnabled(false); 1.285 + suggestion.refreshDrawableState(); 1.286 + } 1.287 +}