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