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    private Rect mPopupPadding;
80
81    public UrlInputView(Context context, AttributeSet attrs, int defStyle) {
82        super(context, attrs, defStyle);
83        TypedArray a = context.obtainStyledAttributes(
84                attrs, com.android.internal.R.styleable.PopupWindow,
85                R.attr.autoCompleteTextViewStyle, 0);
86
87        Drawable popupbg = a.getDrawable(R.styleable.PopupWindow_popupBackground);
88        a.recycle();
89        mPopupPadding = new Rect();
90        popupbg.getPadding(mPopupPadding);
91        init(context);
92    }
93
94    public UrlInputView(Context context, AttributeSet attrs) {
95        this(context, attrs, R.attr.autoCompleteTextViewStyle);
96    }
97
98    public UrlInputView(Context context) {
99        this(context, null);
100    }
101
102    private void init(Context ctx) {
103        mInputManager = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
104        setOnEditorActionListener(this);
105        mAdapter = new SuggestionsAdapter(ctx, this);
106        setAdapter(mAdapter);
107        setSelectAllOnFocus(true);
108        onConfigurationChanged(ctx.getResources().getConfiguration());
109        setThreshold(1);
110        setOnItemClickListener(this);
111        mNeedsUpdate = false;
112        addTextChangedListener(this);
113
114        mState = StateListener.STATE_NORMAL;
115    }
116
117    protected void onFocusChanged(boolean focused, int direction, Rect prevRect) {
118        super.onFocusChanged(focused, direction, prevRect);
119        int state = -1;
120        if (focused) {
121            if (hasSelection()) {
122                state = StateListener.STATE_HIGHLIGHTED;
123            } else {
124                state = StateListener.STATE_EDITED;
125            }
126        } else {
127            // reset the selection state
128            state = StateListener.STATE_NORMAL;
129        }
130        final int s = state;
131        post(new Runnable() {
132            public void run() {
133                changeState(s);
134            }
135        });
136    }
137
138    @Override
139    public boolean onTouchEvent(MotionEvent evt) {
140        boolean hasSelection = hasSelection();
141        boolean res = super.onTouchEvent(evt);
142        if ((MotionEvent.ACTION_DOWN == evt.getActionMasked())
143              && hasSelection) {
144            postDelayed(new Runnable() {
145                public void run() {
146                    changeState(StateListener.STATE_EDITED);
147                }}, POST_DELAY);
148        }
149        return res;
150    }
151
152    /**
153     * check if focus change requires a title bar update
154     */
155    boolean needsUpdate() {
156        return mNeedsUpdate;
157    }
158
159    /**
160     * clear the focus change needs title bar update flag
161     */
162    void clearNeedsUpdate() {
163        mNeedsUpdate = false;
164    }
165
166    void setController(UiController controller) {
167        UrlSelectionActionMode urlSelectionMode
168                = new UrlSelectionActionMode(controller);
169        setCustomSelectionActionModeCallback(urlSelectionMode);
170    }
171
172    void setContainer(View container) {
173        mContainer = container;
174    }
175
176    public void setUrlInputListener(UrlInputListener listener) {
177        mListener = listener;
178    }
179
180    public void setStateListener(StateListener listener) {
181        mStateListener = listener;
182        // update listener
183        changeState(mState);
184    }
185
186    private void changeState(int newState) {
187        mState = newState;
188        if (mStateListener != null) {
189            mStateListener.onStateChanged(mState);
190        }
191    }
192
193    int getState() {
194        return mState;
195    }
196
197    @Override
198    protected void onConfigurationChanged(Configuration config) {
199        super.onConfigurationChanged(config);
200        mLandscape = (config.orientation &
201                Configuration.ORIENTATION_LANDSCAPE) != 0;
202        mAdapter.setLandscapeMode(mLandscape);
203        if (isPopupShowing() && (getVisibility() == View.VISIBLE)) {
204            setupDropDown();
205            performFiltering(getText(), 0);
206        }
207    }
208
209    @Override
210    public void showDropDown() {
211        setupDropDown();
212        super.showDropDown();
213    }
214
215    @Override
216    public void dismissDropDown() {
217        super.dismissDropDown();
218        mAdapter.clearCache();
219    }
220
221    private void setupDropDown() {
222        int width = mContainer != null ? mContainer.getWidth() : getWidth();
223        width += mPopupPadding.left + mPopupPadding.right;
224        if (width != getDropDownWidth()) {
225            setDropDownWidth(width);
226        }
227        int left = getLeft();
228        left += mPopupPadding.left;
229        if (left != -getDropDownHorizontalOffset()) {
230            setDropDownHorizontalOffset(-left);
231        }
232    }
233
234    @Override
235    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
236        finishInput(getText().toString(), null, TYPED);
237        return true;
238    }
239
240    void forceFilter() {
241        showDropDown();
242    }
243
244    void hideIME() {
245        mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);
246    }
247
248    void showIME() {
249        mInputManager.focusIn(this);
250        mInputManager.showSoftInput(this, 0);
251    }
252
253    private void finishInput(String url, String extra, String source) {
254        mNeedsUpdate = true;
255        dismissDropDown();
256        mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);
257        if (TextUtils.isEmpty(url)) {
258            mListener.onDismiss();
259        } else {
260            if (mIncognitoMode && isSearch(url)) {
261                // To prevent logging, intercept this request
262                // TODO: This is a quick hack, refactor this
263                SearchEngine searchEngine = BrowserSettings.getInstance()
264                        .getSearchEngine();
265                if (searchEngine == null) return;
266                SearchEngineInfo engineInfo = SearchEngines
267                        .getSearchEngineInfo(mContext, searchEngine.getName());
268                if (engineInfo == null) return;
269                url = engineInfo.getSearchUriForQuery(url);
270                // mLister.onAction can take it from here without logging
271            }
272            mListener.onAction(url, extra, source);
273        }
274    }
275
276    boolean isSearch(String inUrl) {
277        String url = UrlUtils.fixUrl(inUrl).trim();
278        if (TextUtils.isEmpty(url)) return false;
279
280        if (Patterns.WEB_URL.matcher(url).matches()
281                || UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url).matches()) {
282            return false;
283        }
284        return true;
285    }
286
287    // Completion Listener
288
289    @Override
290    public void onSearch(String search) {
291        mListener.onCopySuggestion(search);
292    }
293
294    @Override
295    public void onSelect(String url, int type, String extra) {
296        finishInput(url, extra, SUGGESTED);
297    }
298
299    @Override
300    public void onItemClick(
301            AdapterView<?> parent, View view, int position, long id) {
302        SuggestItem item = mAdapter.getItem(position);
303        onSelect(SuggestionsAdapter.getSuggestionUrl(item), item.type, item.extra);
304    }
305
306    interface UrlInputListener {
307
308        public void onDismiss();
309
310        public void onAction(String text, String extra, String source);
311
312        public void onCopySuggestion(String text);
313
314    }
315
316    public void setIncognitoMode(boolean incognito) {
317        mIncognitoMode = incognito;
318        mAdapter.setIncognitoMode(mIncognitoMode);
319    }
320
321    @Override
322    public boolean onKeyDown(int keyCode, KeyEvent evt) {
323        if (keyCode == KeyEvent.KEYCODE_ESCAPE && !isInTouchMode()) {
324            finishInput(null, null, null);
325            return true;
326        }
327        return super.onKeyDown(keyCode, evt);
328    }
329
330    public SuggestionsAdapter getAdapter() {
331        return mAdapter;
332    }
333
334    /*
335     * no-op to prevent scrolling of webview when embedded titlebar
336     * gets edited
337     */
338    @Override
339    public boolean requestRectangleOnScreen(Rect rect, boolean immediate) {
340        return false;
341    }
342
343    @Override
344    public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
345
346    @Override
347    public void onTextChanged(CharSequence s, int start, int before, int count) {
348        if (StateListener.STATE_HIGHLIGHTED == mState) {
349            changeState(StateListener.STATE_EDITED);
350        }
351    }
352
353    @Override
354    public void afterTextChanged(Editable s) { }
355
356}
357