Home | History | Annotate | Download | only in webkit
      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 
     17 package android.webkit;
     18 
     19 import android.text.TextUtils;
     20 import android.util.Log;
     21 import android.webkit.WebViewCore.EventHub;
     22 
     23 import java.util.ArrayList;
     24 import java.util.HashMap;
     25 import java.util.List;
     26 
     27 import org.json.JSONArray;
     28 import org.json.JSONException;
     29 import org.json.JSONObject;
     30 import 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  */
     37 final 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