1/* 2 * Copyright (C) 2011 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.text.TextUtils; 20import android.util.Log; 21import android.webkit.WebViewCore.EventHub; 22 23import java.util.ArrayList; 24import java.util.HashMap; 25import java.util.List; 26 27import org.json.JSONArray; 28import org.json.JSONException; 29import org.json.JSONObject; 30import org.json.JSONStringer; 31 32/** 33 * The default implementation of the SearchBox interface. Implemented 34 * as a java bridge object and a javascript adapter that is called into 35 * by the page hosted in the frame. 36 */ 37final class SearchBoxImpl implements SearchBox { 38 private static final String TAG = "WebKit.SearchBoxImpl"; 39 40 /* package */ static final String JS_INTERFACE_NAME = "searchBoxJavaBridge_"; 41 42 /* package */ static final String JS_BRIDGE 43 = "(function()" 44 + "{" 45 + "if (!window.chrome) {" 46 + " window.chrome = {};" 47 + "}" 48 + "if (!window.chrome.searchBox) {" 49 + " var sb = window.chrome.searchBox = {};" 50 + " sb.setSuggestions = function(suggestions) {" 51 + " if (window.searchBoxJavaBridge_) {" 52 + " window.searchBoxJavaBridge_.setSuggestions(JSON.stringify(suggestions));" 53 + " }" 54 + " };" 55 + " sb.setValue = function(valueArray) { sb.value = valueArray[0]; };" 56 + " sb.value = '';" 57 + " sb.x = 0;" 58 + " sb.y = 0;" 59 + " sb.width = 0;" 60 + " sb.height = 0;" 61 + " sb.selectionStart = 0;" 62 + " sb.selectionEnd = 0;" 63 + " sb.verbatim = false;" 64 + "}" 65 + "})();"; 66 67 private static final String SET_QUERY_SCRIPT 68 = "if (window.chrome && window.chrome.searchBox) {" 69 + " window.chrome.searchBox.setValue(%s);" 70 + "}"; 71 72 private static final String SET_VERBATIM_SCRIPT 73 = "if (window.chrome && window.chrome.searchBox) {" 74 + " window.chrome.searchBox.verbatim = %1$s;" 75 + "}"; 76 77 private static final String SET_SELECTION_SCRIPT 78 = "if (window.chrome && window.chrome.searchBox) {" 79 + " var f = window.chrome.searchBox;" 80 + " f.selectionStart = %d" 81 + " f.selectionEnd = %d" 82 + "}"; 83 84 private static final String SET_DIMENSIONS_SCRIPT 85 = "if (window.chrome && window.chrome.searchBox) { " 86 + " var f = window.chrome.searchBox;" 87 + " f.x = %d;" 88 + " f.y = %d;" 89 + " f.width = %d;" 90 + " f.height = %d;" 91 + "}"; 92 93 private static final String DISPATCH_EVENT_SCRIPT 94 = "if (window.chrome && window.chrome.searchBox && window.chrome.searchBox.on%1$s) {" 95 + " window.chrome.searchBox.on%1$s();" 96 + " window.searchBoxJavaBridge_.dispatchCompleteCallback('%1$s', %2$d, true);" 97 + "} else {" 98 + " window.searchBoxJavaBridge_.dispatchCompleteCallback('%1$s', %2$d, false);" 99 + "}"; 100 101 private static final String EVENT_CHANGE = "change"; 102 private static final String EVENT_SUBMIT = "submit"; 103 private static final String EVENT_RESIZE = "resize"; 104 private static final String EVENT_CANCEL = "cancel"; 105 106 private static final String IS_SUPPORTED_SCRIPT 107 = "if (window.searchBoxJavaBridge_) {" 108 + " if (window.chrome && window.chrome.sv) {" 109 + " window.searchBoxJavaBridge_.isSupportedCallback(true);" 110 + " } else {" 111 + " window.searchBoxJavaBridge_.isSupportedCallback(false);" 112 + " }}"; 113 114 private final List<SearchBoxListener> mListeners; 115 private final WebViewCore mWebViewCore; 116 private final CallbackProxy mCallbackProxy; 117 private IsSupportedCallback mSupportedCallback; 118 private int mNextEventId = 1; 119 private final HashMap<Integer, SearchBoxListener> mEventCallbacks; 120 121 SearchBoxImpl(WebViewCore webViewCore, CallbackProxy callbackProxy) { 122 mListeners = new ArrayList<SearchBoxListener>(); 123 mWebViewCore = webViewCore; 124 mCallbackProxy = callbackProxy; 125 mEventCallbacks = new HashMap<Integer, SearchBoxListener>(); 126 } 127 128 @Override 129 public void setQuery(String query) { 130 final String formattedQuery = jsonSerialize(query); 131 if (formattedQuery != null) { 132 final String js = String.format(SET_QUERY_SCRIPT, formattedQuery); 133 dispatchJs(js); 134 } 135 } 136 137 @Override 138 public void setVerbatim(boolean verbatim) { 139 final String js = String.format(SET_VERBATIM_SCRIPT, String.valueOf(verbatim)); 140 dispatchJs(js); 141 } 142 143 144 @Override 145 public void setSelection(int selectionStart, int selectionEnd) { 146 final String js = String.format(SET_SELECTION_SCRIPT, selectionStart, selectionEnd); 147 dispatchJs(js); 148 } 149 150 @Override 151 public void setDimensions(int x, int y, int width, int height) { 152 final String js = String.format(SET_DIMENSIONS_SCRIPT, x, y, width, height); 153 dispatchJs(js); 154 } 155 156 @Override 157 public void onchange(SearchBoxListener callback) { 158 dispatchEvent(EVENT_CHANGE, callback); 159 } 160 161 @Override 162 public void onsubmit(SearchBoxListener callback) { 163 dispatchEvent(EVENT_SUBMIT, callback); 164 } 165 166 @Override 167 public void onresize(SearchBoxListener callback) { 168 dispatchEvent(EVENT_RESIZE, callback); 169 } 170 171 @Override 172 public void oncancel(SearchBoxListener callback) { 173 dispatchEvent(EVENT_CANCEL, callback); 174 } 175 176 private void dispatchEvent(String eventName, SearchBoxListener callback) { 177 int eventId; 178 if (callback != null) { 179 synchronized(this) { 180 eventId = mNextEventId++; 181 mEventCallbacks.put(eventId, callback); 182 } 183 } else { 184 eventId = 0; 185 } 186 final String js = String.format(DISPATCH_EVENT_SCRIPT, eventName, eventId); 187 dispatchJs(js); 188 } 189 190 private void dispatchJs(String js) { 191 mWebViewCore.sendMessage(EventHub.EXECUTE_JS, js); 192 } 193 194 @Override 195 public void addSearchBoxListener(SearchBoxListener l) { 196 synchronized (mListeners) { 197 mListeners.add(l); 198 } 199 } 200 201 @Override 202 public void removeSearchBoxListener(SearchBoxListener l) { 203 synchronized (mListeners) { 204 mListeners.remove(l); 205 } 206 } 207 208 @Override 209 public void isSupported(IsSupportedCallback callback) { 210 mSupportedCallback = callback; 211 dispatchJs(IS_SUPPORTED_SCRIPT); 212 } 213 214 // Called by Javascript through the Java bridge. 215 public void isSupportedCallback(boolean isSupported) { 216 mCallbackProxy.onIsSupportedCallback(isSupported); 217 } 218 219 public void handleIsSupportedCallback(boolean isSupported) { 220 IsSupportedCallback callback = mSupportedCallback; 221 mSupportedCallback = null; 222 if (callback != null) { 223 callback.searchBoxIsSupported(isSupported); 224 } 225 } 226 227 // Called by Javascript through the Java bridge. 228 public void dispatchCompleteCallback(String function, int id, boolean successful) { 229 mCallbackProxy.onSearchboxDispatchCompleteCallback(function, id, successful); 230 } 231 232 public void handleDispatchCompleteCallback(String function, int id, boolean successful) { 233 if (id != 0) { 234 SearchBoxListener listener; 235 synchronized(this) { 236 listener = mEventCallbacks.get(id); 237 mEventCallbacks.remove(id); 238 } 239 if (listener != null) { 240 if (TextUtils.equals(EVENT_CHANGE, function)) { 241 listener.onChangeComplete(successful); 242 } else if (TextUtils.equals(EVENT_SUBMIT, function)) { 243 listener.onSubmitComplete(successful); 244 } else if (TextUtils.equals(EVENT_RESIZE, function)) { 245 listener.onResizeComplete(successful); 246 } else if (TextUtils.equals(EVENT_CANCEL, function)) { 247 listener.onCancelComplete(successful); 248 } 249 } 250 } 251 } 252 253 // This is used as a hackish alternative to javascript escaping. 254 // There appears to be no such functionality in the core framework. 255 private static String jsonSerialize(String query) { 256 JSONStringer stringer = new JSONStringer(); 257 try { 258 stringer.array().value(query).endArray(); 259 } catch (JSONException e) { 260 Log.w(TAG, "Error serializing query : " + query); 261 return null; 262 } 263 return stringer.toString(); 264 } 265 266 // Called by Javascript through the Java bridge. 267 public void setSuggestions(String jsonArguments) { 268 if (jsonArguments == null) { 269 return; 270 } 271 272 String query = null; 273 List<String> suggestions = new ArrayList<String>(); 274 try { 275 JSONObject suggestionsJson = new JSONObject(jsonArguments); 276 query = suggestionsJson.getString("query"); 277 278 final JSONArray suggestionsArray = suggestionsJson.getJSONArray("suggestions"); 279 for (int i = 0; i < suggestionsArray.length(); ++i) { 280 final JSONObject suggestion = suggestionsArray.getJSONObject(i); 281 final String value = suggestion.getString("value"); 282 if (value != null) { 283 suggestions.add(value); 284 } 285 // We currently ignore the "type" of the suggestion. This isn't 286 // documented anywhere in the API documents. 287 // final String type = suggestions.getString("type"); 288 } 289 } catch (JSONException je) { 290 Log.w(TAG, "Error parsing json [" + jsonArguments + "], exception = " + je); 291 return; 292 } 293 294 mCallbackProxy.onSearchboxSuggestionsReceived(query, suggestions); 295 } 296 297 /* package */ void handleSuggestions(String query, List<String> suggestions) { 298 synchronized (mListeners) { 299 for (int i = mListeners.size() - 1; i >= 0; i--) { 300 mListeners.get(i).onSuggestionsReceived(query, suggestions); 301 } 302 } 303 } 304} 305