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