Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko.home;
8 import org.mozilla.gecko.R;
9 import org.mozilla.gecko.Telemetry;
10 import org.mozilla.gecko.TelemetryContract;
11 import org.mozilla.gecko.home.BrowserSearch.OnEditSuggestionListener;
12 import org.mozilla.gecko.home.BrowserSearch.OnSearchListener;
13 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
14 import org.mozilla.gecko.util.StringUtils;
15 import org.mozilla.gecko.widget.AnimatedHeightLayout;
16 import org.mozilla.gecko.widget.FaviconView;
17 import org.mozilla.gecko.widget.FlowLayout;
19 import android.content.Context;
20 import android.util.AttributeSet;
21 import android.view.KeyEvent;
22 import android.view.LayoutInflater;
23 import android.view.View;
24 import android.view.animation.AlphaAnimation;
25 import android.widget.ImageView;
26 import android.widget.LinearLayout;
27 import android.widget.TextView;
29 import java.util.EnumSet;
31 class SearchEngineRow extends AnimatedHeightLayout {
32 // Duration for fade-in animation
33 private static final int ANIMATION_DURATION = 250;
35 // Inner views
36 private final FlowLayout mSuggestionView;
37 private final FaviconView mIconView;
38 private final LinearLayout mUserEnteredView;
39 private final TextView mUserEnteredTextView;
41 // Inflater used when updating from suggestions
42 private final LayoutInflater mInflater;
44 // Search engine associated with this view
45 private SearchEngine mSearchEngine;
47 // Event listeners for suggestion views
48 private final OnClickListener mClickListener;
49 private final OnLongClickListener mLongClickListener;
51 // On URL open listener
52 private OnUrlOpenListener mUrlOpenListener;
54 // On search listener
55 private OnSearchListener mSearchListener;
57 // On edit suggestion listener
58 private OnEditSuggestionListener mEditSuggestionListener;
60 // Selected suggestion view
61 private int mSelectedView = 0;
63 public SearchEngineRow(Context context) {
64 this(context, null);
65 }
67 public SearchEngineRow(Context context, AttributeSet attrs) {
68 this(context, attrs, 0);
69 }
71 public SearchEngineRow(Context context, AttributeSet attrs, int defStyle) {
72 super(context, attrs, defStyle);
74 mClickListener = new OnClickListener() {
75 @Override
76 public void onClick(View v) {
77 final String suggestion = getSuggestionTextFromView(v);
79 // If we're not clicking the user-entered view (the first suggestion item)
80 // and the search matches a URL pattern, go to that URL. Otherwise, do a
81 // search for the term.
82 if (v != mUserEnteredView && !StringUtils.isSearchQuery(suggestion, false)) {
83 if (mUrlOpenListener != null) {
84 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "url");
86 mUrlOpenListener.onUrlOpen(suggestion, EnumSet.noneOf(OnUrlOpenListener.Flags.class));
87 }
88 } else if (mSearchListener != null) {
89 if (v == mUserEnteredView) {
90 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user");
91 } else {
92 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "engine");
93 }
94 mSearchListener.onSearch(mSearchEngine, suggestion);
95 }
96 }
97 };
99 mLongClickListener = new OnLongClickListener() {
100 @Override
101 public boolean onLongClick(View v) {
102 if (mEditSuggestionListener != null) {
103 final String suggestion = getSuggestionTextFromView(v);
104 mEditSuggestionListener.onEditSuggestion(suggestion);
105 return true;
106 }
108 return false;
109 }
110 };
112 mInflater = LayoutInflater.from(context);
113 mInflater.inflate(R.layout.search_engine_row, this);
115 mSuggestionView = (FlowLayout) findViewById(R.id.suggestion_layout);
116 mIconView = (FaviconView) findViewById(R.id.suggestion_icon);
118 // User-entered search term is first suggestion
119 mUserEnteredView = (LinearLayout) findViewById(R.id.suggestion_user_entered);
120 mUserEnteredView.setOnClickListener(mClickListener);
122 mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text);
123 }
125 private void setDescriptionOnSuggestion(View v, String suggestion) {
126 v.setContentDescription(getResources().getString(R.string.suggestion_for_engine,
127 mSearchEngine.name, suggestion));
128 }
130 private String getSuggestionTextFromView(View v) {
131 final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text);
132 return suggestionText.getText().toString();
133 }
135 private void setSuggestionOnView(View v, String suggestion) {
136 final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text);
137 suggestionText.setText(suggestion);
138 setDescriptionOnSuggestion(suggestionText, suggestion);
139 }
141 /**
142 * Perform a search for the user-entered term.
143 */
144 public void performUserEnteredSearch() {
145 String searchTerm = getSuggestionTextFromView(mUserEnteredView);
146 if (mSearchListener != null) {
147 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user");
148 mSearchListener.onSearch(mSearchEngine, searchTerm);
149 }
150 }
152 public void setSearchTerm(String searchTerm) {
153 mUserEnteredTextView.setText(searchTerm);
155 // mSearchEngine is not set in the first call to this method; the content description
156 // is instead initially set in updateFromSearchEngine.
157 if (mSearchEngine != null) {
158 setDescriptionOnSuggestion(mUserEnteredTextView, searchTerm);
159 }
160 }
162 public void setOnUrlOpenListener(OnUrlOpenListener listener) {
163 mUrlOpenListener = listener;
164 }
166 public void setOnSearchListener(OnSearchListener listener) {
167 mSearchListener = listener;
168 }
170 public void setOnEditSuggestionListener(OnEditSuggestionListener listener) {
171 mEditSuggestionListener = listener;
172 }
174 public void updateFromSearchEngine(SearchEngine searchEngine, boolean animate) {
175 // Update search engine reference.
176 mSearchEngine = searchEngine;
178 // Set the search engine icon (e.g., Google) for the row.
179 mIconView.updateAndScaleImage(mSearchEngine.getIcon(), mSearchEngine.getEngineIdentifier());
181 // Set the initial content description.
182 setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString());
184 // Add additional suggestions given by this engine.
185 final int recycledSuggestionCount = mSuggestionView.getChildCount();
187 int suggestionCounter = 0;
188 for (String suggestion : mSearchEngine.getSuggestions()) {
189 final View suggestionItem;
191 // Reuse suggestion views from recycled view, if possible.
192 if (suggestionCounter + 1 < recycledSuggestionCount) {
193 suggestionItem = mSuggestionView.getChildAt(suggestionCounter + 1);
194 suggestionItem.setVisibility(View.VISIBLE);
195 } else {
196 suggestionItem = mInflater.inflate(R.layout.suggestion_item, null);
198 suggestionItem.setOnClickListener(mClickListener);
199 suggestionItem.setOnLongClickListener(mLongClickListener);
201 final ImageView magnifier =
202 (ImageView) suggestionItem.findViewById(R.id.suggestion_magnifier);
203 magnifier.setVisibility(View.GONE);
205 mSuggestionView.addView(suggestionItem);
206 }
208 setSuggestionOnView(suggestionItem, suggestion);
210 if (animate) {
211 AlphaAnimation anim = new AlphaAnimation(0, 1);
212 anim.setDuration(ANIMATION_DURATION);
213 anim.setStartOffset(suggestionCounter * ANIMATION_DURATION);
214 suggestionItem.startAnimation(anim);
215 }
217 ++suggestionCounter;
218 }
220 // Hide extra suggestions that have been recycled.
221 for (int i = suggestionCounter + 1; i < recycledSuggestionCount; ++i) {
222 mSuggestionView.getChildAt(i).setVisibility(View.GONE);
223 }
225 // Make sure mSelectedView is still valid.
226 if (mSelectedView >= mSuggestionView.getChildCount()) {
227 mSelectedView = mSuggestionView.getChildCount() - 1;
228 }
229 }
231 @Override
232 public boolean onKeyDown(int keyCode, android.view.KeyEvent event) {
233 final View suggestion = mSuggestionView.getChildAt(mSelectedView);
235 if (event.getAction() != android.view.KeyEvent.ACTION_DOWN) {
236 return false;
237 }
239 switch (event.getKeyCode()) {
240 case KeyEvent.KEYCODE_DPAD_RIGHT:
241 final View nextSuggestion = mSuggestionView.getChildAt(mSelectedView + 1);
242 if (nextSuggestion != null) {
243 changeSelectedSuggestion(suggestion, nextSuggestion);
244 mSelectedView++;
245 return true;
246 }
247 break;
249 case KeyEvent.KEYCODE_DPAD_LEFT:
250 final View prevSuggestion = mSuggestionView.getChildAt(mSelectedView - 1);
251 if (prevSuggestion != null) {
252 changeSelectedSuggestion(suggestion, prevSuggestion);
253 mSelectedView--;
254 return true;
255 }
256 break;
258 case KeyEvent.KEYCODE_BUTTON_A:
259 // TODO: handle long pressing for editing suggestions
260 return suggestion.performClick();
261 }
263 return false;
264 }
266 private void changeSelectedSuggestion(View oldSuggestion, View newSuggestion) {
267 oldSuggestion.setDuplicateParentStateEnabled(false);
268 newSuggestion.setDuplicateParentStateEnabled(true);
269 oldSuggestion.refreshDrawableState();
270 newSuggestion.refreshDrawableState();
271 }
273 public void onSelected() {
274 mSelectedView = 0;
275 mUserEnteredView.setDuplicateParentStateEnabled(true);
276 mUserEnteredView.refreshDrawableState();
277 }
279 public void onDeselected() {
280 final View suggestion = mSuggestionView.getChildAt(mSelectedView);
281 suggestion.setDuplicateParentStateEnabled(false);
282 suggestion.refreshDrawableState();
283 }
284 }