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