| /* |
| * Copyright (C) 2011 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.text.TextUtils; |
| import android.util.Log; |
| import android.webkit.WebViewCore.EventHub; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| |
| import org.json.JSONArray; |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| import org.json.JSONStringer; |
| |
| /** |
| * The default implementation of the SearchBox interface. Implemented |
| * as a java bridge object and a javascript adapter that is called into |
| * by the page hosted in the frame. |
| */ |
| final class SearchBoxImpl implements SearchBox { |
| private static final String TAG = "WebKit.SearchBoxImpl"; |
| |
| /* package */ static final String JS_INTERFACE_NAME = "searchBoxJavaBridge_"; |
| |
| /* package */ static final String JS_BRIDGE |
| = "(function()" |
| + "{" |
| + "if (!window.chrome) {" |
| + " window.chrome = {};" |
| + "}" |
| + "if (!window.chrome.searchBox) {" |
| + " var sb = window.chrome.searchBox = {};" |
| + " sb.setSuggestions = function(suggestions) {" |
| + " if (window.searchBoxJavaBridge_) {" |
| + " window.searchBoxJavaBridge_.setSuggestions(JSON.stringify(suggestions));" |
| + " }" |
| + " };" |
| + " sb.setValue = function(valueArray) { sb.value = valueArray[0]; };" |
| + " sb.value = '';" |
| + " sb.x = 0;" |
| + " sb.y = 0;" |
| + " sb.width = 0;" |
| + " sb.height = 0;" |
| + " sb.selectionStart = 0;" |
| + " sb.selectionEnd = 0;" |
| + " sb.verbatim = false;" |
| + "}" |
| + "})();"; |
| |
| private static final String SET_QUERY_SCRIPT |
| = "if (window.chrome && window.chrome.searchBox) {" |
| + " window.chrome.searchBox.setValue(%s);" |
| + "}"; |
| |
| private static final String SET_VERBATIM_SCRIPT |
| = "if (window.chrome && window.chrome.searchBox) {" |
| + " window.chrome.searchBox.verbatim = %1$s;" |
| + "}"; |
| |
| private static final String SET_SELECTION_SCRIPT |
| = "if (window.chrome && window.chrome.searchBox) {" |
| + " var f = window.chrome.searchBox;" |
| + " f.selectionStart = %d" |
| + " f.selectionEnd = %d" |
| + "}"; |
| |
| private static final String SET_DIMENSIONS_SCRIPT |
| = "if (window.chrome && window.chrome.searchBox) { " |
| + " var f = window.chrome.searchBox;" |
| + " f.x = %d;" |
| + " f.y = %d;" |
| + " f.width = %d;" |
| + " f.height = %d;" |
| + "}"; |
| |
| private static final String DISPATCH_EVENT_SCRIPT |
| = "if (window.chrome && window.chrome.searchBox && window.chrome.searchBox.on%1$s) {" |
| + " window.chrome.searchBox.on%1$s();" |
| + " window.searchBoxJavaBridge_.dispatchCompleteCallback('%1$s', %2$d, true);" |
| + "} else {" |
| + " window.searchBoxJavaBridge_.dispatchCompleteCallback('%1$s', %2$d, false);" |
| + "}"; |
| |
| private static final String EVENT_CHANGE = "change"; |
| private static final String EVENT_SUBMIT = "submit"; |
| private static final String EVENT_RESIZE = "resize"; |
| private static final String EVENT_CANCEL = "cancel"; |
| |
| private static final String IS_SUPPORTED_SCRIPT |
| = "if (window.searchBoxJavaBridge_) {" |
| + " if (window.chrome && window.chrome.sv) {" |
| + " window.searchBoxJavaBridge_.isSupportedCallback(true);" |
| + " } else {" |
| + " window.searchBoxJavaBridge_.isSupportedCallback(false);" |
| + " }}"; |
| |
| private final List<SearchBoxListener> mListeners; |
| private final WebViewCore mWebViewCore; |
| private final CallbackProxy mCallbackProxy; |
| private IsSupportedCallback mSupportedCallback; |
| private int mNextEventId = 1; |
| private final HashMap<Integer, SearchBoxListener> mEventCallbacks; |
| |
| SearchBoxImpl(WebViewCore webViewCore, CallbackProxy callbackProxy) { |
| mListeners = new ArrayList<SearchBoxListener>(); |
| mWebViewCore = webViewCore; |
| mCallbackProxy = callbackProxy; |
| mEventCallbacks = new HashMap<Integer, SearchBoxListener>(); |
| } |
| |
| @Override |
| public void setQuery(String query) { |
| final String formattedQuery = jsonSerialize(query); |
| if (formattedQuery != null) { |
| final String js = String.format(SET_QUERY_SCRIPT, formattedQuery); |
| dispatchJs(js); |
| } |
| } |
| |
| @Override |
| public void setVerbatim(boolean verbatim) { |
| final String js = String.format(SET_VERBATIM_SCRIPT, String.valueOf(verbatim)); |
| dispatchJs(js); |
| } |
| |
| |
| @Override |
| public void setSelection(int selectionStart, int selectionEnd) { |
| final String js = String.format(SET_SELECTION_SCRIPT, selectionStart, selectionEnd); |
| dispatchJs(js); |
| } |
| |
| @Override |
| public void setDimensions(int x, int y, int width, int height) { |
| final String js = String.format(SET_DIMENSIONS_SCRIPT, x, y, width, height); |
| dispatchJs(js); |
| } |
| |
| @Override |
| public void onchange(SearchBoxListener callback) { |
| dispatchEvent(EVENT_CHANGE, callback); |
| } |
| |
| @Override |
| public void onsubmit(SearchBoxListener callback) { |
| dispatchEvent(EVENT_SUBMIT, callback); |
| } |
| |
| @Override |
| public void onresize(SearchBoxListener callback) { |
| dispatchEvent(EVENT_RESIZE, callback); |
| } |
| |
| @Override |
| public void oncancel(SearchBoxListener callback) { |
| dispatchEvent(EVENT_CANCEL, callback); |
| } |
| |
| private void dispatchEvent(String eventName, SearchBoxListener callback) { |
| int eventId; |
| if (callback != null) { |
| synchronized(this) { |
| eventId = mNextEventId++; |
| mEventCallbacks.put(eventId, callback); |
| } |
| } else { |
| eventId = 0; |
| } |
| final String js = String.format(DISPATCH_EVENT_SCRIPT, eventName, eventId); |
| dispatchJs(js); |
| } |
| |
| private void dispatchJs(String js) { |
| mWebViewCore.sendMessage(EventHub.EXECUTE_JS, js); |
| } |
| |
| @Override |
| public void addSearchBoxListener(SearchBoxListener l) { |
| synchronized (mListeners) { |
| mListeners.add(l); |
| } |
| } |
| |
| @Override |
| public void removeSearchBoxListener(SearchBoxListener l) { |
| synchronized (mListeners) { |
| mListeners.remove(l); |
| } |
| } |
| |
| @Override |
| public void isSupported(IsSupportedCallback callback) { |
| mSupportedCallback = callback; |
| dispatchJs(IS_SUPPORTED_SCRIPT); |
| } |
| |
| // Called by Javascript through the Java bridge. |
| public void isSupportedCallback(boolean isSupported) { |
| mCallbackProxy.onIsSupportedCallback(isSupported); |
| } |
| |
| public void handleIsSupportedCallback(boolean isSupported) { |
| IsSupportedCallback callback = mSupportedCallback; |
| mSupportedCallback = null; |
| if (callback != null) { |
| callback.searchBoxIsSupported(isSupported); |
| } |
| } |
| |
| // Called by Javascript through the Java bridge. |
| public void dispatchCompleteCallback(String function, int id, boolean successful) { |
| mCallbackProxy.onSearchboxDispatchCompleteCallback(function, id, successful); |
| } |
| |
| public void handleDispatchCompleteCallback(String function, int id, boolean successful) { |
| if (id != 0) { |
| SearchBoxListener listener; |
| synchronized(this) { |
| listener = mEventCallbacks.get(id); |
| mEventCallbacks.remove(id); |
| } |
| if (listener != null) { |
| if (TextUtils.equals(EVENT_CHANGE, function)) { |
| listener.onChangeComplete(successful); |
| } else if (TextUtils.equals(EVENT_SUBMIT, function)) { |
| listener.onSubmitComplete(successful); |
| } else if (TextUtils.equals(EVENT_RESIZE, function)) { |
| listener.onResizeComplete(successful); |
| } else if (TextUtils.equals(EVENT_CANCEL, function)) { |
| listener.onCancelComplete(successful); |
| } |
| } |
| } |
| } |
| |
| // This is used as a hackish alternative to javascript escaping. |
| // There appears to be no such functionality in the core framework. |
| private static String jsonSerialize(String query) { |
| JSONStringer stringer = new JSONStringer(); |
| try { |
| stringer.array().value(query).endArray(); |
| } catch (JSONException e) { |
| Log.w(TAG, "Error serializing query : " + query); |
| return null; |
| } |
| return stringer.toString(); |
| } |
| |
| // Called by Javascript through the Java bridge. |
| public void setSuggestions(String jsonArguments) { |
| if (jsonArguments == null) { |
| return; |
| } |
| |
| String query = null; |
| List<String> suggestions = new ArrayList<String>(); |
| try { |
| JSONObject suggestionsJson = new JSONObject(jsonArguments); |
| query = suggestionsJson.getString("query"); |
| |
| final JSONArray suggestionsArray = suggestionsJson.getJSONArray("suggestions"); |
| for (int i = 0; i < suggestionsArray.length(); ++i) { |
| final JSONObject suggestion = suggestionsArray.getJSONObject(i); |
| final String value = suggestion.getString("value"); |
| if (value != null) { |
| suggestions.add(value); |
| } |
| // We currently ignore the "type" of the suggestion. This isn't |
| // documented anywhere in the API documents. |
| // final String type = suggestions.getString("type"); |
| } |
| } catch (JSONException je) { |
| Log.w(TAG, "Error parsing json [" + jsonArguments + "], exception = " + je); |
| return; |
| } |
| |
| mCallbackProxy.onSearchboxSuggestionsReceived(query, suggestions); |
| } |
| |
| /* package */ void handleSuggestions(String query, List<String> suggestions) { |
| synchronized (mListeners) { |
| for (int i = mListeners.size() - 1; i >= 0; i--) { |
| mListeners.get(i).onSuggestionsReceived(query, suggestions); |
| } |
| } |
| } |
| } |