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