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 
17 package android.webkit;
18 
19 import android.annotation.SystemApi;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.Point;
23 import android.graphics.Rect;
24 import android.text.Editable;
25 import android.text.Selection;
26 import android.text.Spannable;
27 import android.text.TextWatcher;
28 import android.view.ActionMode;
29 import android.view.LayoutInflater;
30 import android.view.Menu;
31 import android.view.MenuItem;
32 import android.view.View;
33 import android.view.inputmethod.InputMethodManager;
34 import android.widget.EditText;
35 import android.widget.TextView;
36 
37 /**
38  * @hide
39  */
40 @SystemApi
41 public 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 
FindActionModeCallback(Context context)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 
finish()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      */
setText(String text)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      */
setWebView(WebView webView)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
onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting)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      */
findNext(boolean next)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      */
findAll()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 
showSoftInput()156     public void showSoftInput() {
157         mInput.startGettingWindowFocus(mEditText.getRootView());
158         mInput.focusIn(mEditText);
159         mInput.showSoftInput(mEditText, 0);
160     }
161 
updateMatchCount(int matchIndex, int matchCount, boolean isEmptyFind)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      */
updateMatchesString()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
onClick(View v)191     public void onClick(View v) {
192         findNext(true);
193     }
194 
195     // ActionMode.Callback implementation
196 
197     @Override
onCreateActionMode(ActionMode mode, Menu menu)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
onDestroyActionMode(ActionMode mode)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
onPrepareActionMode(ActionMode mode, Menu menu)229     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
230         return false;
231     }
232 
233     @Override
onActionItemClicked(ActionMode mode, MenuItem item)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
beforeTextChanged(CharSequence s, int start, int count, int after)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
onTextChanged(CharSequence s, int start, int before, int count)264     public void onTextChanged(CharSequence s,
265                               int start,
266                               int before,
267                               int count) {
268         findAll();
269     }
270 
271     @Override
afterTextChanged(Editable s)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();
getActionModeGlobalBottom()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
onCreateActionMode(ActionMode mode, Menu menu)292         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
293             return false;
294         }
295 
296         @Override
onPrepareActionMode(ActionMode mode, Menu menu)297         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
298             return false;
299         }
300 
301         @Override
onActionItemClicked(ActionMode mode, MenuItem item)302         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
303             return false;
304         }
305 
306         @Override
onDestroyActionMode(ActionMode mode)307         public void onDestroyActionMode(ActionMode mode) {
308         }
309     }
310 }
311