|
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/. */ |
|
5 |
|
6 package org.mozilla.gecko.home; |
|
7 |
|
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; |
|
18 |
|
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; |
|
28 |
|
29 import java.util.EnumSet; |
|
30 |
|
31 class SearchEngineRow extends AnimatedHeightLayout { |
|
32 // Duration for fade-in animation |
|
33 private static final int ANIMATION_DURATION = 250; |
|
34 |
|
35 // Inner views |
|
36 private final FlowLayout mSuggestionView; |
|
37 private final FaviconView mIconView; |
|
38 private final LinearLayout mUserEnteredView; |
|
39 private final TextView mUserEnteredTextView; |
|
40 |
|
41 // Inflater used when updating from suggestions |
|
42 private final LayoutInflater mInflater; |
|
43 |
|
44 // Search engine associated with this view |
|
45 private SearchEngine mSearchEngine; |
|
46 |
|
47 // Event listeners for suggestion views |
|
48 private final OnClickListener mClickListener; |
|
49 private final OnLongClickListener mLongClickListener; |
|
50 |
|
51 // On URL open listener |
|
52 private OnUrlOpenListener mUrlOpenListener; |
|
53 |
|
54 // On search listener |
|
55 private OnSearchListener mSearchListener; |
|
56 |
|
57 // On edit suggestion listener |
|
58 private OnEditSuggestionListener mEditSuggestionListener; |
|
59 |
|
60 // Selected suggestion view |
|
61 private int mSelectedView = 0; |
|
62 |
|
63 public SearchEngineRow(Context context) { |
|
64 this(context, null); |
|
65 } |
|
66 |
|
67 public SearchEngineRow(Context context, AttributeSet attrs) { |
|
68 this(context, attrs, 0); |
|
69 } |
|
70 |
|
71 public SearchEngineRow(Context context, AttributeSet attrs, int defStyle) { |
|
72 super(context, attrs, defStyle); |
|
73 |
|
74 mClickListener = new OnClickListener() { |
|
75 @Override |
|
76 public void onClick(View v) { |
|
77 final String suggestion = getSuggestionTextFromView(v); |
|
78 |
|
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"); |
|
85 |
|
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 }; |
|
98 |
|
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 } |
|
107 |
|
108 return false; |
|
109 } |
|
110 }; |
|
111 |
|
112 mInflater = LayoutInflater.from(context); |
|
113 mInflater.inflate(R.layout.search_engine_row, this); |
|
114 |
|
115 mSuggestionView = (FlowLayout) findViewById(R.id.suggestion_layout); |
|
116 mIconView = (FaviconView) findViewById(R.id.suggestion_icon); |
|
117 |
|
118 // User-entered search term is first suggestion |
|
119 mUserEnteredView = (LinearLayout) findViewById(R.id.suggestion_user_entered); |
|
120 mUserEnteredView.setOnClickListener(mClickListener); |
|
121 |
|
122 mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text); |
|
123 } |
|
124 |
|
125 private void setDescriptionOnSuggestion(View v, String suggestion) { |
|
126 v.setContentDescription(getResources().getString(R.string.suggestion_for_engine, |
|
127 mSearchEngine.name, suggestion)); |
|
128 } |
|
129 |
|
130 private String getSuggestionTextFromView(View v) { |
|
131 final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text); |
|
132 return suggestionText.getText().toString(); |
|
133 } |
|
134 |
|
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 } |
|
140 |
|
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 } |
|
151 |
|
152 public void setSearchTerm(String searchTerm) { |
|
153 mUserEnteredTextView.setText(searchTerm); |
|
154 |
|
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 } |
|
161 |
|
162 public void setOnUrlOpenListener(OnUrlOpenListener listener) { |
|
163 mUrlOpenListener = listener; |
|
164 } |
|
165 |
|
166 public void setOnSearchListener(OnSearchListener listener) { |
|
167 mSearchListener = listener; |
|
168 } |
|
169 |
|
170 public void setOnEditSuggestionListener(OnEditSuggestionListener listener) { |
|
171 mEditSuggestionListener = listener; |
|
172 } |
|
173 |
|
174 public void updateFromSearchEngine(SearchEngine searchEngine, boolean animate) { |
|
175 // Update search engine reference. |
|
176 mSearchEngine = searchEngine; |
|
177 |
|
178 // Set the search engine icon (e.g., Google) for the row. |
|
179 mIconView.updateAndScaleImage(mSearchEngine.getIcon(), mSearchEngine.getEngineIdentifier()); |
|
180 |
|
181 // Set the initial content description. |
|
182 setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString()); |
|
183 |
|
184 // Add additional suggestions given by this engine. |
|
185 final int recycledSuggestionCount = mSuggestionView.getChildCount(); |
|
186 |
|
187 int suggestionCounter = 0; |
|
188 for (String suggestion : mSearchEngine.getSuggestions()) { |
|
189 final View suggestionItem; |
|
190 |
|
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); |
|
197 |
|
198 suggestionItem.setOnClickListener(mClickListener); |
|
199 suggestionItem.setOnLongClickListener(mLongClickListener); |
|
200 |
|
201 final ImageView magnifier = |
|
202 (ImageView) suggestionItem.findViewById(R.id.suggestion_magnifier); |
|
203 magnifier.setVisibility(View.GONE); |
|
204 |
|
205 mSuggestionView.addView(suggestionItem); |
|
206 } |
|
207 |
|
208 setSuggestionOnView(suggestionItem, suggestion); |
|
209 |
|
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 } |
|
216 |
|
217 ++suggestionCounter; |
|
218 } |
|
219 |
|
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 } |
|
224 |
|
225 // Make sure mSelectedView is still valid. |
|
226 if (mSelectedView >= mSuggestionView.getChildCount()) { |
|
227 mSelectedView = mSuggestionView.getChildCount() - 1; |
|
228 } |
|
229 } |
|
230 |
|
231 @Override |
|
232 public boolean onKeyDown(int keyCode, android.view.KeyEvent event) { |
|
233 final View suggestion = mSuggestionView.getChildAt(mSelectedView); |
|
234 |
|
235 if (event.getAction() != android.view.KeyEvent.ACTION_DOWN) { |
|
236 return false; |
|
237 } |
|
238 |
|
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; |
|
248 |
|
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; |
|
257 |
|
258 case KeyEvent.KEYCODE_BUTTON_A: |
|
259 // TODO: handle long pressing for editing suggestions |
|
260 return suggestion.performClick(); |
|
261 } |
|
262 |
|
263 return false; |
|
264 } |
|
265 |
|
266 private void changeSelectedSuggestion(View oldSuggestion, View newSuggestion) { |
|
267 oldSuggestion.setDuplicateParentStateEnabled(false); |
|
268 newSuggestion.setDuplicateParentStateEnabled(true); |
|
269 oldSuggestion.refreshDrawableState(); |
|
270 newSuggestion.refreshDrawableState(); |
|
271 } |
|
272 |
|
273 public void onSelected() { |
|
274 mSelectedView = 0; |
|
275 mUserEnteredView.setDuplicateParentStateEnabled(true); |
|
276 mUserEnteredView.refreshDrawableState(); |
|
277 } |
|
278 |
|
279 public void onDeselected() { |
|
280 final View suggestion = mSuggestionView.getChildAt(mSelectedView); |
|
281 suggestion.setDuplicateParentStateEnabled(false); |
|
282 suggestion.refreshDrawableState(); |
|
283 } |
|
284 } |