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