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.NonNull;
20 import android.annotation.SystemApi;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.graphics.Point;
24 import android.graphics.Rect;
25 import android.text.Editable;
26 import android.text.Selection;
27 import android.text.Spannable;
28 import android.text.TextWatcher;
29 import android.view.ActionMode;
30 import android.view.LayoutInflater;
31 import android.view.Menu;
32 import android.view.MenuItem;
33 import android.view.View;
34 import android.view.inputmethod.InputMethodManager;
35 import android.widget.EditText;
36 import android.widget.TextView;
37 
38 /**
39  * @hide
40  */
41 @SystemApi
42 public 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 
FindActionModeCallback(Context context)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 
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.
93      *
94      * @param webView an implementation of WebView
95      */
setWebView(@onNull WebView webView)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
onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting)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      */
findNext(boolean next)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      */
findAll()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 
showSoftInput()158     public void showSoftInput() {
159         if (mEditText.requestFocus()) {
160             mInput.showSoftInput(mEditText, 0);
161         }
162     }
163 
updateMatchCount(int matchIndex, int matchCount, boolean isEmptyFind)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      */
updateMatchesString()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
onClick(View v)193     public void onClick(View v) {
194         findNext(true);
195     }
196 
197     // ActionMode.Callback implementation
198 
199     @Override
onCreateActionMode(ActionMode mode, Menu menu)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
onDestroyActionMode(ActionMode mode)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
onPrepareActionMode(ActionMode mode, Menu menu)231     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
232         return false;
233     }
234 
235     @Override
onActionItemClicked(ActionMode mode, MenuItem item)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
beforeTextChanged(CharSequence s, int start, int count, int after)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
onTextChanged(CharSequence s, int start, int before, int count)266     public void onTextChanged(CharSequence s,
267                               int start,
268                               int before,
269                               int count) {
270         findAll();
271     }
272 
273     @Override
afterTextChanged(Editable s)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();
getActionModeGlobalBottom()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
onCreateActionMode(ActionMode mode, Menu menu)294         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
295             return false;
296         }
297 
298         @Override
onPrepareActionMode(ActionMode mode, Menu menu)299         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
300             return false;
301         }
302 
303         @Override
onActionItemClicked(ActionMode mode, MenuItem item)304         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
305             return false;
306         }
307 
308         @Override
onDestroyActionMode(ActionMode mode)309         public void onDestroyActionMode(ActionMode mode) {
310         }
311     }
312 }
313