|  | /* | 
|  | * Copyright (C) 2010 The Android Open Source Project | 
|  | * | 
|  | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | * you may not use this file except in compliance with the License. | 
|  | * You may obtain a copy of the License at | 
|  | * | 
|  | *      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, software | 
|  | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | * See the License for the specific language governing permissions and | 
|  | * limitations under the License. | 
|  | */ | 
|  |  | 
|  | package android.webkit; | 
|  |  | 
|  | import android.annotation.NonNull; | 
|  | import android.annotation.SystemApi; | 
|  | import android.content.Context; | 
|  | import android.content.res.Resources; | 
|  | import android.graphics.Point; | 
|  | import android.graphics.Rect; | 
|  | import android.text.Editable; | 
|  | import android.text.Selection; | 
|  | import android.text.Spannable; | 
|  | import android.text.TextWatcher; | 
|  | import android.util.PluralsMessageFormatter; | 
|  | import android.view.ActionMode; | 
|  | import android.view.LayoutInflater; | 
|  | import android.view.Menu; | 
|  | import android.view.MenuItem; | 
|  | import android.view.View; | 
|  | import android.view.inputmethod.InputMethodManager; | 
|  | import android.widget.EditText; | 
|  | import android.widget.TextView; | 
|  |  | 
|  | import com.android.internal.R; | 
|  |  | 
|  | import java.util.HashMap; | 
|  | import java.util.Map; | 
|  |  | 
|  | /** | 
|  | * @hide | 
|  | */ | 
|  | @SystemApi | 
|  | public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, | 
|  | View.OnClickListener, WebView.FindListener { | 
|  | private View mCustomView; | 
|  | private EditText mEditText; | 
|  | private TextView mMatches; | 
|  | private WebView mWebView; | 
|  | private InputMethodManager mInput; | 
|  | private Resources mResources; | 
|  | private boolean mMatchesFound; | 
|  | private int mNumberOfMatches; | 
|  | private int mActiveMatchIndex; | 
|  | private ActionMode mActionMode; | 
|  |  | 
|  | public FindActionModeCallback(Context context) { | 
|  | mCustomView = LayoutInflater.from(context).inflate( | 
|  | com.android.internal.R.layout.webview_find, null); | 
|  | mEditText = (EditText) mCustomView.findViewById( | 
|  | com.android.internal.R.id.edit); | 
|  | mEditText.setCustomSelectionActionModeCallback(new NoAction()); | 
|  | mEditText.setOnClickListener(this); | 
|  | setText(""); | 
|  | mMatches = (TextView) mCustomView.findViewById( | 
|  | com.android.internal.R.id.matches); | 
|  | mInput = context.getSystemService(InputMethodManager.class); | 
|  | mResources = context.getResources(); | 
|  | } | 
|  |  | 
|  | public void finish() { | 
|  | mActionMode.finish(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Place text in the text field so it can be searched for.  Need to press | 
|  | * the find next or find previous button to find all of the matches. | 
|  | */ | 
|  | public void setText(String text) { | 
|  | mEditText.setText(text); | 
|  | Spannable span = (Spannable) mEditText.getText(); | 
|  | int length = span.length(); | 
|  | // Ideally, we would like to set the selection to the whole field, | 
|  | // but this brings up the Text selection CAB, which dismisses this | 
|  | // one. | 
|  | Selection.setSelection(span, length, length); | 
|  | // Necessary each time we set the text, so that this will watch | 
|  | // changes to it. | 
|  | span.setSpan(this, 0, length, Spannable.SPAN_INCLUSIVE_INCLUSIVE); | 
|  | mMatchesFound = false; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Set the WebView to search. | 
|  | * | 
|  | * @param webView an implementation of WebView | 
|  | */ | 
|  | public void setWebView(@NonNull WebView webView) { | 
|  | if (null == webView) { | 
|  | throw new AssertionError("WebView supplied to " | 
|  | + "FindActionModeCallback cannot be null"); | 
|  | } | 
|  | mWebView = webView; | 
|  | mWebView.setFindDialogFindListener(this); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, | 
|  | boolean isDoneCounting) { | 
|  | if (isDoneCounting) { | 
|  | updateMatchCount(activeMatchOrdinal, numberOfMatches, numberOfMatches == 0); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Move the highlight to the next match. | 
|  | * @param next If {@code true}, find the next match further down in the document. | 
|  | *             If {@code false}, find the previous match, up in the document. | 
|  | */ | 
|  | private void findNext(boolean next) { | 
|  | if (mWebView == null) { | 
|  | throw new AssertionError( | 
|  | "No WebView for FindActionModeCallback::findNext"); | 
|  | } | 
|  | if (!mMatchesFound) { | 
|  | findAll(); | 
|  | return; | 
|  | } | 
|  | if (0 == mNumberOfMatches) { | 
|  | // There are no matches, so moving to the next match will not do | 
|  | // anything. | 
|  | return; | 
|  | } | 
|  | mWebView.findNext(next); | 
|  | updateMatchesString(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Highlight all the instances of the string from mEditText in mWebView. | 
|  | */ | 
|  | public void findAll() { | 
|  | if (mWebView == null) { | 
|  | throw new AssertionError( | 
|  | "No WebView for FindActionModeCallback::findAll"); | 
|  | } | 
|  | CharSequence find = mEditText.getText(); | 
|  | if (0 == find.length()) { | 
|  | mWebView.clearMatches(); | 
|  | mMatches.setVisibility(View.GONE); | 
|  | mMatchesFound = false; | 
|  | mWebView.findAll(null); | 
|  | } else { | 
|  | mMatchesFound = true; | 
|  | mMatches.setVisibility(View.INVISIBLE); | 
|  | mNumberOfMatches = 0; | 
|  | mWebView.findAllAsync(find.toString()); | 
|  | } | 
|  | } | 
|  |  | 
|  | public void showSoftInput() { | 
|  | if (mEditText.requestFocus()) { | 
|  | mInput.showSoftInput(mEditText, 0); | 
|  | } | 
|  | } | 
|  |  | 
|  | public void updateMatchCount(int matchIndex, int matchCount, boolean isEmptyFind) { | 
|  | if (!isEmptyFind) { | 
|  | mNumberOfMatches = matchCount; | 
|  | mActiveMatchIndex = matchIndex; | 
|  | updateMatchesString(); | 
|  | } else { | 
|  | mMatches.setVisibility(View.GONE); | 
|  | mNumberOfMatches = 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Update the string which tells the user how many matches were found, and | 
|  | * which match is currently highlighted. | 
|  | */ | 
|  | private void updateMatchesString() { | 
|  | if (mNumberOfMatches == 0) { | 
|  | mMatches.setText(com.android.internal.R.string.no_matches); | 
|  | } else { | 
|  | Map<String, Object> arguments = new HashMap<>(); | 
|  | arguments.put("count", mActiveMatchIndex + 1); | 
|  | arguments.put("total", mNumberOfMatches); | 
|  |  | 
|  | mMatches.setText(PluralsMessageFormatter.format( | 
|  | mResources, | 
|  | arguments, | 
|  | R.string.matches_found)); | 
|  | } | 
|  | mMatches.setVisibility(View.VISIBLE); | 
|  | } | 
|  |  | 
|  | // OnClickListener implementation | 
|  |  | 
|  | @Override | 
|  | public void onClick(View v) { | 
|  | findNext(true); | 
|  | } | 
|  |  | 
|  | // ActionMode.Callback implementation | 
|  |  | 
|  | @Override | 
|  | public boolean onCreateActionMode(ActionMode mode, Menu menu) { | 
|  | if (!mode.isUiFocusable()) { | 
|  | // If the action mode we're running in is not focusable the user | 
|  | // will not be able to type into the find on page field. This | 
|  | // should only come up when we're running in a dialog which is | 
|  | // already less than ideal; disable the option for now. | 
|  | return false; | 
|  | } | 
|  |  | 
|  | mode.setCustomView(mCustomView); | 
|  | mode.getMenuInflater().inflate(com.android.internal.R.menu.webview_find, | 
|  | menu); | 
|  | mActionMode = mode; | 
|  | Editable edit = mEditText.getText(); | 
|  | Selection.setSelection(edit, edit.length()); | 
|  | mMatches.setVisibility(View.GONE); | 
|  | mMatchesFound = false; | 
|  | mMatches.setText("0"); | 
|  | mEditText.requestFocus(); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onDestroyActionMode(ActionMode mode) { | 
|  | mActionMode = null; | 
|  | mWebView.notifyFindDialogDismissed(); | 
|  | mWebView.setFindDialogFindListener(null); | 
|  | mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onPrepareActionMode(ActionMode mode, Menu menu) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onActionItemClicked(ActionMode mode, MenuItem item) { | 
|  | if (mWebView == null) { | 
|  | throw new AssertionError( | 
|  | "No WebView for FindActionModeCallback::onActionItemClicked"); | 
|  | } | 
|  | mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); | 
|  | switch(item.getItemId()) { | 
|  | case com.android.internal.R.id.find_prev: | 
|  | findNext(false); | 
|  | break; | 
|  | case com.android.internal.R.id.find_next: | 
|  | findNext(true); | 
|  | break; | 
|  | default: | 
|  | return false; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // TextWatcher implementation | 
|  |  | 
|  | @Override | 
|  | public void beforeTextChanged(CharSequence s, | 
|  | int start, | 
|  | int count, | 
|  | int after) { | 
|  | // Does nothing.  Needed to implement TextWatcher. | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onTextChanged(CharSequence s, | 
|  | int start, | 
|  | int before, | 
|  | int count) { | 
|  | findAll(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void afterTextChanged(Editable s) { | 
|  | // Does nothing.  Needed to implement TextWatcher. | 
|  | } | 
|  |  | 
|  | private Rect mGlobalVisibleRect = new Rect(); | 
|  | private Point mGlobalVisibleOffset = new Point(); | 
|  | public int getActionModeGlobalBottom() { | 
|  | if (mActionMode == null) { | 
|  | return 0; | 
|  | } | 
|  | View view = (View) mCustomView.getParent(); | 
|  | if (view == null) { | 
|  | view = mCustomView; | 
|  | } | 
|  | view.getGlobalVisibleRect(mGlobalVisibleRect, mGlobalVisibleOffset); | 
|  | return mGlobalVisibleRect.bottom; | 
|  | } | 
|  |  | 
|  | public static class NoAction implements ActionMode.Callback { | 
|  | @Override | 
|  | public boolean onCreateActionMode(ActionMode mode, Menu menu) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onPrepareActionMode(ActionMode mode, Menu menu) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onActionItemClicked(ActionMode mode, MenuItem item) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onDestroyActionMode(ActionMode mode) { | 
|  | } | 
|  | } | 
|  | } |