1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.browser;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.content.res.TypedArray;
22import android.graphics.Rect;
23import android.graphics.drawable.Drawable;
24import android.text.Editable;
25import android.text.TextUtils;
26import android.text.TextWatcher;
27import android.util.AttributeSet;
28import android.util.Patterns;
29import android.view.KeyEvent;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.inputmethod.InputMethodManager;
33import android.widget.AdapterView;
34import android.widget.AdapterView.OnItemClickListener;
35import android.widget.AutoCompleteTextView;
36import android.widget.TextView;
37import android.widget.TextView.OnEditorActionListener;
38
39import com.android.browser.SuggestionsAdapter.CompletionListener;
40import com.android.browser.SuggestionsAdapter.SuggestItem;
41import com.android.browser.search.SearchEngine;
42import com.android.browser.search.SearchEngineInfo;
43import com.android.browser.search.SearchEngines;
44import com.android.internal.R;
45
46import java.util.List;
47
48/**
49 * url/search input view
50 * handling suggestions
51 */
52public class UrlInputView extends AutoCompleteTextView
53        implements OnEditorActionListener,
54        CompletionListener, OnItemClickListener, TextWatcher {
55
56    static final String TYPED = "browser-type";
57    static final String SUGGESTED = "browser-suggest";
58
59    static final int POST_DELAY = 100;
60
61    static interface StateListener {
62        static final int STATE_NORMAL = 0;
63        static final int STATE_HIGHLIGHTED = 1;
64        static final int STATE_EDITED = 2;
65
66        public void onStateChanged(int state);
67    }
68
69    private UrlInputListener   mListener;
70    private InputMethodManager mInputManager;
71    private SuggestionsAdapter mAdapter;
72    private View mContainer;
73    private boolean mLandscape;
74    private boolean mIncognitoMode;
75    private boolean mNeedsUpdate;
76
77    private int mState;
78    private StateListener mStateListener;
79
80    public UrlInputView(Context context, AttributeSet attrs, int defStyle) {
81        super(context, attrs, defStyle);
82        init(context);
83    }
84
85    public UrlInputView(Context context, AttributeSet attrs) {
86        this(context, attrs, R.attr.autoCompleteTextViewStyle);
87    }
88
89    public UrlInputView(Context context) {
90        this(context, null);
91    }
92
93    private void init(Context ctx) {
94        mInputManager = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
95        setOnEditorActionListener(this);
96        mAdapter = new SuggestionsAdapter(ctx, this);
97        setAdapter(mAdapter);
98        setSelectAllOnFocus(true);
99        onConfigurationChanged(ctx.getResources().getConfiguration());
100        setThreshold(1);
101        setOnItemClickListener(this);
102        mNeedsUpdate = false;
103        addTextChangedListener(this);
104        setDropDownAnchor(com.android.browser.R.id.taburlbar);
105        mState = StateListener.STATE_NORMAL;
106    }
107
108    protected void onFocusChanged(boolean focused, int direction, Rect prevRect) {
109        super.onFocusChanged(focused, direction, prevRect);
110        int state = -1;
111        if (focused) {
112            if (hasSelection()) {
113                state = StateListener.STATE_HIGHLIGHTED;
114            } else {
115                state = StateListener.STATE_EDITED;
116            }
117        } else {
118            // reset the selection state
119            state = StateListener.STATE_NORMAL;
120        }
121        final int s = state;
122        post(new Runnable() {
123            public void run() {
124                changeState(s);
125            }
126        });
127    }
128
129    @Override
130    public boolean onTouchEvent(MotionEvent evt) {
131        boolean hasSelection = hasSelection();
132        boolean res = super.onTouchEvent(evt);
133        if ((MotionEvent.ACTION_DOWN == evt.getActionMasked())
134              && hasSelection) {
135            postDelayed(new Runnable() {
136                public void run() {
137                    changeState(StateListener.STATE_EDITED);
138                }}, POST_DELAY);
139        }
140        return res;
141    }
142
143    /**
144     * check if focus change requires a title bar update
145     */
146    boolean needsUpdate() {
147        return mNeedsUpdate;
148    }
149
150    /**
151     * clear the focus change needs title bar update flag
152     */
153    void clearNeedsUpdate() {
154        mNeedsUpdate = false;
155    }
156
157    void setController(UiController controller) {
158        UrlSelectionActionMode urlSelectionMode
159                = new UrlSelectionActionMode(controller);
160        setCustomSelectionActionModeCallback(urlSelectionMode);
161    }
162
163    void setContainer(View container) {
164        mContainer = container;
165    }
166
167    public void setUrlInputListener(UrlInputListener listener) {
168        mListener = listener;
169    }
170
171    public void setStateListener(StateListener listener) {
172        mStateListener = listener;
173        // update listener
174        changeState(mState);
175    }
176
177    private void changeState(int newState) {
178        mState = newState;
179        if (mStateListener != null) {
180            mStateListener.onStateChanged(mState);
181        }
182    }
183
184    int getState() {
185        return mState;
186    }
187
188    @Override
189    protected void onConfigurationChanged(Configuration config) {
190        super.onConfigurationChanged(config);
191        mLandscape = (config.orientation &
192                Configuration.ORIENTATION_LANDSCAPE) != 0;
193        mAdapter.setLandscapeMode(mLandscape);
194        if (isPopupShowing() && (getVisibility() == View.VISIBLE)) {
195            dismissDropDown();
196            showDropDown();
197            performFiltering(getText(), 0);
198        }
199    }
200
201    @Override
202    public void dismissDropDown() {
203        super.dismissDropDown();
204        mAdapter.clearCache();
205    }
206
207    @Override
208    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
209        finishInput(getText().toString(), null, TYPED);
210        return true;
211    }
212
213    void forceFilter() {
214        showDropDown();
215    }
216
217    void hideIME() {
218        mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);
219    }
220
221    void showIME() {
222        mInputManager.focusIn(this);
223        mInputManager.showSoftInput(this, 0);
224    }
225
226    private void finishInput(String url, String extra, String source) {
227        mNeedsUpdate = true;
228        dismissDropDown();
229        mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);
230        if (TextUtils.isEmpty(url)) {
231            mListener.onDismiss();
232        } else {
233            if (mIncognitoMode && isSearch(url)) {
234                // To prevent logging, intercept this request
235                // TODO: This is a quick hack, refactor this
236                SearchEngine searchEngine = BrowserSettings.getInstance()
237                        .getSearchEngine();
238                if (searchEngine == null) return;
239                SearchEngineInfo engineInfo = SearchEngines
240                        .getSearchEngineInfo(mContext, searchEngine.getName());
241                if (engineInfo == null) return;
242                url = engineInfo.getSearchUriForQuery(url);
243                // mLister.onAction can take it from here without logging
244            }
245            mListener.onAction(url, extra, source);
246        }
247    }
248
249    boolean isSearch(String inUrl) {
250        String url = UrlUtils.fixUrl(inUrl).trim();
251        if (TextUtils.isEmpty(url)) return false;
252
253        if (Patterns.WEB_URL.matcher(url).matches()
254                || UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url).matches()) {
255            return false;
256        }
257        return true;
258    }
259
260    // Completion Listener
261
262    @Override
263    public void onSearch(String search) {
264        mListener.onCopySuggestion(search);
265    }
266
267    @Override
268    public void onSelect(String url, int type, String extra) {
269        finishInput(url, extra, SUGGESTED);
270    }
271
272    @Override
273    public void onItemClick(
274            AdapterView<?> parent, View view, int position, long id) {
275        SuggestItem item = mAdapter.getItem(position);
276        onSelect(SuggestionsAdapter.getSuggestionUrl(item), item.type, item.extra);
277    }
278
279    interface UrlInputListener {
280
281        public void onDismiss();
282
283        public void onAction(String text, String extra, String source);
284
285        public void onCopySuggestion(String text);
286
287    }
288
289    public void setIncognitoMode(boolean incognito) {
290        mIncognitoMode = incognito;
291        mAdapter.setIncognitoMode(mIncognitoMode);
292    }
293
294    @Override
295    public boolean onKeyDown(int keyCode, KeyEvent evt) {
296        if (keyCode == KeyEvent.KEYCODE_ESCAPE && !isInTouchMode()) {
297            finishInput(null, null, null);
298            return true;
299        }
300        return super.onKeyDown(keyCode, evt);
301    }
302
303    public SuggestionsAdapter getAdapter() {
304        return mAdapter;
305    }
306
307    /*
308     * no-op to prevent scrolling of webview when embedded titlebar
309     * gets edited
310     */
311    @Override
312    public boolean requestRectangleOnScreen(Rect rect, boolean immediate) {
313        return false;
314    }
315
316    @Override
317    public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
318
319    @Override
320    public void onTextChanged(CharSequence s, int start, int before, int count) {
321        if (StateListener.STATE_HIGHLIGHTED == mState) {
322            changeState(StateListener.STATE_EDITED);
323        }
324    }
325
326    @Override
327    public void afterTextChanged(Editable s) { }
328
329}
330