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 = context.getSystemService(InputMethodManager.class);
65         mResources = context.getResources();
66     }
67 
finish()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      */
setText(String text)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      */
setWebView(WebView webView)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
onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting)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      */
findNext(boolean next)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      */
findAll()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 
showSoftInput()155     public void showSoftInput() {
156         if (mEditText.requestFocus()) {
157             mInput.showSoftInput(mEditText, 0);
158         }
159     }
160 
updateMatchCount(int matchIndex, int matchCount, boolean isEmptyFind)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      */
updateMatchesString()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
onClick(View v)190     public void onClick(View v) {
191         findNext(true);
192     }
193 
194     // ActionMode.Callback implementation
195 
196     @Override
onCreateActionMode(ActionMode mode, Menu menu)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
onDestroyActionMode(ActionMode mode)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
onPrepareActionMode(ActionMode mode, Menu menu)228     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
229         return false;
230     }
231 
232     @Override
onActionItemClicked(ActionMode mode, MenuItem item)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
beforeTextChanged(CharSequence s, int start, int count, int after)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
onTextChanged(CharSequence s, int start, int before, int count)263     public void onTextChanged(CharSequence s,
264                               int start,
265                               int before,
266                               int count) {
267         findAll();
268     }
269 
270     @Override
afterTextChanged(Editable s)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();
getActionModeGlobalBottom()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
onCreateActionMode(ActionMode mode, Menu menu)291         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
292             return false;
293         }
294 
295         @Override
onPrepareActionMode(ActionMode mode, Menu menu)296         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
297             return false;
298         }
299 
300         @Override
onActionItemClicked(ActionMode mode, MenuItem item)301         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
302             return false;
303         }
304 
305         @Override
onDestroyActionMode(ActionMode mode)306         public void onDestroyActionMode(ActionMode mode) {
307         }
308     }
309 }
310