1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 package org.mozilla.search.autocomplete;
6 
7 import org.mozilla.gecko.R;
8 import org.mozilla.gecko.Telemetry;
9 import org.mozilla.gecko.TelemetryContract;
10 import org.mozilla.gecko.gfx.BitmapUtils;
11 import org.mozilla.gecko.search.SearchEngine;
12 
13 import android.content.Context;
14 import android.graphics.Bitmap;
15 import android.graphics.Color;
16 import android.graphics.PorterDuff;
17 import android.graphics.PorterDuffColorFilter;
18 import android.graphics.drawable.BitmapDrawable;
19 import android.graphics.drawable.Drawable;
20 import android.text.Editable;
21 import android.text.TextWatcher;
22 import android.util.AttributeSet;
23 import android.view.KeyEvent;
24 import android.view.LayoutInflater;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.inputmethod.EditorInfo;
28 import android.view.inputmethod.InputMethodManager;
29 import android.widget.EditText;
30 import android.widget.FrameLayout;
31 import android.widget.ImageButton;
32 import android.widget.ImageView;
33 import android.widget.TextView;
34 
35 public class SearchBar extends FrameLayout {
36 
37     private final EditText editText;
38     private final ImageButton clearButton;
39     private final ImageView engineIcon;
40 
41     private final Drawable focusedBackground;
42     private final Drawable defaultBackground;
43 
44     private final InputMethodManager inputMethodManager;
45 
46     private TextListener listener;
47 
48     private boolean active;
49 
50     public interface TextListener {
onChange(String text)51         public void onChange(String text);
onSubmit(String text)52         public void onSubmit(String text);
onFocusChange(boolean hasFocus)53         public void onFocusChange(boolean hasFocus);
54     }
55 
56     // Deprecation warnings suppressed to allow building with API level 22
57     @SuppressWarnings("deprecation")
SearchBar(Context context, AttributeSet attrs)58     public SearchBar(Context context, AttributeSet attrs) {
59         super(context, attrs);
60 
61         LayoutInflater.from(context).inflate(R.layout.search_bar, this);
62 
63         editText = (EditText) findViewById(R.id.edit_text);
64         editText.addTextChangedListener(new TextWatcher() {
65             @Override
66             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
67             }
68 
69             @Override
70             public void onTextChanged(CharSequence s, int start, int before, int count) {
71             }
72 
73             @Override
74             public void afterTextChanged(Editable s) {
75                 if (listener != null) {
76                     listener.onChange(s.toString());
77                 }
78 
79                 updateClearButtonVisibility();
80             }
81         });
82 
83         // Attach a listener for the "search" key on the keyboard.
84         editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
85             @Override
86             public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
87                 if (listener != null &&
88                     (actionId == EditorInfo.IME_ACTION_UNSPECIFIED || actionId == EditorInfo.IME_ACTION_SEARCH)) {
89                     // The user searched without using search engine suggestions.
90                     Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH, TelemetryContract.Method.ACTIONBAR, "text");
91                     listener.onSubmit(v.getText().toString());
92                     return true;
93                 }
94                 return false;
95             }
96         });
97 
98         editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
99             @Override
100             public void onFocusChange(View v, boolean hasFocus) {
101                 if (listener != null) {
102                     listener.onFocusChange(hasFocus);
103                 }
104             }
105         });
106 
107         clearButton = (ImageButton) findViewById(R.id.clear_button);
108         clearButton.setOnClickListener(new View.OnClickListener() {
109             @Override
110             public void onClick(View v) {
111                 editText.setText("");
112             }
113         });
114         engineIcon = (ImageView) findViewById(R.id.engine_icon);
115 
116         focusedBackground = getResources().getDrawable(R.drawable.edit_text_focused);
117         defaultBackground = getResources().getDrawable(R.drawable.edit_text_default);
118 
119         inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
120     }
121 
setText(String text)122     public void setText(String text) {
123         editText.setText(text);
124 
125         // Move cursor to end of search input.
126         editText.setSelection(text.length());
127     }
128 
getText()129     public String getText() {
130         return editText.getText().toString();
131     }
132 
setEngine(SearchEngine engine)133     public void setEngine(SearchEngine engine) {
134         final String iconURL = engine.getIconURL();
135         final Bitmap bitmap = BitmapUtils.getBitmapFromDataURI(iconURL);
136         final BitmapDrawable d = new BitmapDrawable(getResources(), bitmap);
137         engineIcon.setImageDrawable(d);
138         engineIcon.setContentDescription(engine.getName());
139 
140         // Update the focused background color.
141         int color = BitmapUtils.getDominantColor(bitmap);
142 
143         // BitmapUtils#getDominantColor ignores black and white pixels, but it will
144         // return white if no dominant color was found. We don't want to create a
145         // white underline for the search bar, so we default to black instead.
146         if (color == Color.WHITE) {
147             color = Color.BLACK;
148         }
149         focusedBackground.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
150 
151         editText.setHint(getResources().getString(R.string.search_bar_hint, engine.getName()));
152     }
153 
154     @SuppressWarnings("deprecation")
setActive(boolean active)155     public void setActive(boolean active) {
156         if (this.active == active) {
157             return;
158         }
159         this.active = active;
160 
161         updateClearButtonVisibility();
162 
163         editText.setFocusable(active);
164         editText.setFocusableInTouchMode(active);
165 
166         final int leftDrawable = active ? R.drawable.search_icon_active : R.drawable.search_icon_inactive;
167         editText.setCompoundDrawablesWithIntrinsicBounds(leftDrawable, 0, 0, 0);
168 
169         // We can't use a selector drawable because we apply a color filter to the focused
170         // background at run time.
171         // TODO: setBackgroundDrawable is deprecated in API level 16
172         editText.setBackgroundDrawable(active ? focusedBackground : defaultBackground);
173 
174         if (active) {
175             editText.requestFocus();
176             inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
177         } else {
178             editText.clearFocus();
179             inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0);
180         }
181     }
182 
updateClearButtonVisibility()183     private void updateClearButtonVisibility() {
184         // Only show the clear button when there is text in the input.
185         final boolean visible = active && (editText.getText().length() > 0);
186         clearButton.setVisibility(visible ? View.VISIBLE : View.GONE);
187         engineIcon.setVisibility(visible ? View.GONE : View.VISIBLE);
188     }
189 
setTextListener(TextListener listener)190     public void setTextListener(TextListener listener) {
191         this.listener = listener;
192     }
193 
194     @Override
onInterceptTouchEvent(MotionEvent e)195     public boolean onInterceptTouchEvent(MotionEvent e) {
196         // When the view is active, pass touch events to child views.
197         // Otherwise, intercept touch events to allow click listeners on the view to
198         // fire no matter where the user clicks.
199         return !active;
200     }
201 }
202