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 android.webkit;
18
19import android.annotation.SystemApi;
20import android.content.Context;
21import android.content.res.Resources;
22import android.graphics.Point;
23import android.graphics.Rect;
24import android.text.Editable;
25import android.text.Selection;
26import android.text.Spannable;
27import android.text.TextWatcher;
28import android.view.ActionMode;
29import android.view.LayoutInflater;
30import android.view.Menu;
31import android.view.MenuItem;
32import android.view.View;
33import android.view.inputmethod.InputMethodManager;
34import android.widget.EditText;
35import android.widget.TextView;
36
37/**
38 * @hide
39 */
40@SystemApi
41public class FindActionModeCallback implements ActionMode.Callback, TextWatcher,
42        View.OnClickListener, WebView.FindListener {
43    private View mCustomView;
44    private EditText mEditText;
45    private TextView mMatches;
46    private WebView mWebView;
47    private InputMethodManager mInput;
48    private Resources mResources;
49    private boolean mMatchesFound;
50    private int mNumberOfMatches;
51    private int mActiveMatchIndex;
52    private ActionMode mActionMode;
53
54    public FindActionModeCallback(Context context) {
55        mCustomView = LayoutInflater.from(context).inflate(
56                com.android.internal.R.layout.webview_find, null);
57        mEditText = (EditText) mCustomView.findViewById(
58                com.android.internal.R.id.edit);
59        mEditText.setCustomSelectionActionModeCallback(new NoAction());
60        mEditText.setOnClickListener(this);
61        setText("");
62        mMatches = (TextView) mCustomView.findViewById(
63                com.android.internal.R.id.matches);
64        mInput = context.getSystemService(InputMethodManager.class);
65        mResources = context.getResources();
66    }
67
68    public void finish() {
69        mActionMode.finish();
70    }
71
72    /*
73     * Place text in the text field so it can be searched for.  Need to press
74     * the find next or find previous button to find all of the matches.
75     */
76    public void setText(String text) {
77        mEditText.setText(text);
78        Spannable span = (Spannable) mEditText.getText();
79        int length = span.length();
80        // Ideally, we would like to set the selection to the whole field,
81        // but this brings up the Text selection CAB, which dismisses this
82        // one.
83        Selection.setSelection(span, length, length);
84        // Necessary each time we set the text, so that this will watch
85        // changes to it.
86        span.setSpan(this, 0, length, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
87        mMatchesFound = false;
88    }
89
90    /*
91     * Set the WebView to search.  Must be non null.
92     */
93    public void setWebView(WebView webView) {
94        if (null == webView) {
95            throw new AssertionError("WebView supplied to "
96                    + "FindActionModeCallback cannot be null");
97        }
98        mWebView = webView;
99        mWebView.setFindDialogFindListener(this);
100    }
101
102    @Override
103    public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
104            boolean isDoneCounting) {
105        if (isDoneCounting) {
106            updateMatchCount(activeMatchOrdinal, numberOfMatches, numberOfMatches == 0);
107        }
108    }
109
110    /*
111     * Move the highlight to the next match.
112     * @param next If true, find the next match further down in the document.
113     *             If false, find the previous match, up in the document.
114     */
115    private void findNext(boolean next) {
116        if (mWebView == null) {
117            throw new AssertionError(
118                    "No WebView for FindActionModeCallback::findNext");
119        }
120        if (!mMatchesFound) {
121            findAll();
122            return;
123        }
124        if (0 == mNumberOfMatches) {
125            // There are no matches, so moving to the next match will not do
126            // anything.
127            return;
128        }
129        mWebView.findNext(next);
130        updateMatchesString();
131    }
132
133    /*
134     * Highlight all the instances of the string from mEditText in mWebView.
135     */
136    public void findAll() {
137        if (mWebView == null) {
138            throw new AssertionError(
139                    "No WebView for FindActionModeCallback::findAll");
140        }
141        CharSequence find = mEditText.getText();
142        if (0 == find.length()) {
143            mWebView.clearMatches();
144            mMatches.setVisibility(View.GONE);
145            mMatchesFound = false;
146            mWebView.findAll(null);
147        } else {
148            mMatchesFound = true;
149            mMatches.setVisibility(View.INVISIBLE);
150            mNumberOfMatches = 0;
151            mWebView.findAllAsync(find.toString());
152        }
153    }
154
155    public void showSoftInput() {
156        if (mEditText.requestFocus()) {
157            mInput.showSoftInput(mEditText, 0);
158        }
159    }
160
161    public void updateMatchCount(int matchIndex, int matchCount, boolean isEmptyFind) {
162        if (!isEmptyFind) {
163            mNumberOfMatches = matchCount;
164            mActiveMatchIndex = matchIndex;
165            updateMatchesString();
166        } else {
167            mMatches.setVisibility(View.GONE);
168            mNumberOfMatches = 0;
169        }
170    }
171
172    /*
173     * Update the string which tells the user how many matches were found, and
174     * which match is currently highlighted.
175     */
176    private void updateMatchesString() {
177        if (mNumberOfMatches == 0) {
178            mMatches.setText(com.android.internal.R.string.no_matches);
179        } else {
180            mMatches.setText(mResources.getQuantityString(
181                com.android.internal.R.plurals.matches_found, mNumberOfMatches,
182                mActiveMatchIndex + 1, mNumberOfMatches));
183        }
184        mMatches.setVisibility(View.VISIBLE);
185    }
186
187    // OnClickListener implementation
188
189    @Override
190    public void onClick(View v) {
191        findNext(true);
192    }
193
194    // ActionMode.Callback implementation
195
196    @Override
197    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
198        if (!mode.isUiFocusable()) {
199            // If the action mode we're running in is not focusable the user
200            // will not be able to type into the find on page field. This
201            // should only come up when we're running in a dialog which is
202            // already less than ideal; disable the option for now.
203            return false;
204        }
205
206        mode.setCustomView(mCustomView);
207        mode.getMenuInflater().inflate(com.android.internal.R.menu.webview_find,
208                menu);
209        mActionMode = mode;
210        Editable edit = mEditText.getText();
211        Selection.setSelection(edit, edit.length());
212        mMatches.setVisibility(View.GONE);
213        mMatchesFound = false;
214        mMatches.setText("0");
215        mEditText.requestFocus();
216        return true;
217    }
218
219    @Override
220    public void onDestroyActionMode(ActionMode mode) {
221        mActionMode = null;
222        mWebView.notifyFindDialogDismissed();
223        mWebView.setFindDialogFindListener(null);
224        mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0);
225    }
226
227    @Override
228    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
229        return false;
230    }
231
232    @Override
233    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
234        if (mWebView == null) {
235            throw new AssertionError(
236                    "No WebView for FindActionModeCallback::onActionItemClicked");
237        }
238        mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0);
239        switch(item.getItemId()) {
240            case com.android.internal.R.id.find_prev:
241                findNext(false);
242                break;
243            case com.android.internal.R.id.find_next:
244                findNext(true);
245                break;
246            default:
247                return false;
248        }
249        return true;
250    }
251
252    // TextWatcher implementation
253
254    @Override
255    public void beforeTextChanged(CharSequence s,
256                                  int start,
257                                  int count,
258                                  int after) {
259        // Does nothing.  Needed to implement TextWatcher.
260    }
261
262    @Override
263    public void onTextChanged(CharSequence s,
264                              int start,
265                              int before,
266                              int count) {
267        findAll();
268    }
269
270    @Override
271    public void afterTextChanged(Editable s) {
272        // Does nothing.  Needed to implement TextWatcher.
273    }
274
275    private Rect mGlobalVisibleRect = new Rect();
276    private Point mGlobalVisibleOffset = new Point();
277    public int getActionModeGlobalBottom() {
278        if (mActionMode == null) {
279            return 0;
280        }
281        View view = (View) mCustomView.getParent();
282        if (view == null) {
283            view = mCustomView;
284        }
285        view.getGlobalVisibleRect(mGlobalVisibleRect, mGlobalVisibleOffset);
286        return mGlobalVisibleRect.bottom;
287    }
288
289    public static class NoAction implements ActionMode.Callback {
290        @Override
291        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
292            return false;
293        }
294
295        @Override
296        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
297            return false;
298        }
299
300        @Override
301        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
302            return false;
303        }
304
305        @Override
306        public void onDestroyActionMode(ActionMode mode) {
307        }
308    }
309}
310