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