1 /* 2 * Copyright (C) 2016 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.googlecode.android_scripting.interpreter.html; 18 19 import android.app.Activity; 20 import android.content.res.Resources; 21 import android.graphics.Bitmap; 22 import android.graphics.drawable.BitmapDrawable; 23 import android.net.Uri; 24 import android.view.ContextMenu; 25 import android.view.ContextMenu.ContextMenuInfo; 26 import android.view.Menu; 27 import android.view.View; 28 import android.view.Window; 29 import android.webkit.JsPromptResult; 30 import android.webkit.JsResult; 31 import android.webkit.WebChromeClient; 32 import android.webkit.WebView; 33 import android.webkit.WebViewClient; 34 35 import com.googlecode.android_scripting.FileUtils; 36 import com.googlecode.android_scripting.Log; 37 import com.googlecode.android_scripting.SingleThreadExecutor; 38 import com.googlecode.android_scripting.event.Event; 39 import com.googlecode.android_scripting.event.EventObserver; 40 import com.googlecode.android_scripting.facade.EventFacade; 41 import com.googlecode.android_scripting.facade.ui.UiFacade; 42 import com.googlecode.android_scripting.future.FutureActivityTask; 43 import com.googlecode.android_scripting.interpreter.InterpreterConstants; 44 import com.googlecode.android_scripting.jsonrpc.JsonBuilder; 45 import com.googlecode.android_scripting.jsonrpc.JsonRpcResult; 46 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 47 import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager; 48 import com.googlecode.android_scripting.rpc.MethodDescriptor; 49 import com.googlecode.android_scripting.rpc.RpcError; 50 51 import java.io.File; 52 import java.io.IOException; 53 import java.util.HashMap; 54 import java.util.HashSet; 55 import java.util.Map; 56 import java.util.Set; 57 import java.util.concurrent.ExecutorService; 58 59 import org.json.JSONArray; 60 import org.json.JSONException; 61 import org.json.JSONObject; 62 63 /** 64 * @author Alexey Reznichenko (alexey.reznichenko (at) gmail.com) 65 */ 66 public class HtmlActivityTask extends FutureActivityTask<Void> { 67 68 private static final String HTTP = "http"; 69 private static final String ANDROID_PROTOTYPE_JS = 70 "Android.prototype.%1$s = function(var_args) { " 71 + "return this._call(\"%1$s\", Array.prototype.slice.call(arguments)); };"; 72 73 private static final String PREFIX = "file://"; 74 private static final String BASE_URL = PREFIX + InterpreterConstants.SCRIPTS_ROOT; 75 76 private final RpcReceiverManager mReceiverManager; 77 private final String mJsonSource; 78 private final String mAndroidJsSource; 79 private final String mAPIWrapperSource; 80 private final String mUrl; 81 private final JavaScriptWrapper mWrapper; 82 private final HtmlEventObserver mObserver; 83 private final UiFacade mUiFacade; 84 private ChromeClient mChromeClient; 85 private WebView mView; 86 private MyWebViewClient mWebViewClient; 87 private static HtmlActivityTask reference; 88 private boolean mDestroyManager; 89 90 public HtmlActivityTask(RpcReceiverManager manager, String androidJsSource, String jsonSource, 91 String url, boolean destroyManager) { 92 reference = this; 93 mReceiverManager = manager; 94 mJsonSource = jsonSource; 95 mAndroidJsSource = androidJsSource; 96 mAPIWrapperSource = generateAPIWrapper(); 97 mWrapper = new JavaScriptWrapper(); 98 mObserver = new HtmlEventObserver(); 99 mReceiverManager.getReceiver(EventFacade.class).addGlobalEventObserver(mObserver); 100 mUiFacade = mReceiverManager.getReceiver(UiFacade.class); 101 mUrl = url; 102 mDestroyManager = destroyManager; 103 } 104 105 public RpcReceiverManager getRpcReceiverManager() { 106 return mReceiverManager; 107 } 108 109 /* 110 * New WebviewClient 111 */ 112 private class MyWebViewClient extends WebViewClient { 113 @Override 114 public boolean shouldOverrideUrlLoading(WebView view, String url) { 115 /* 116 * if (Uri.parse(url).getHost().equals("www.example.com")) { 117 * // This is my web site, so do not 118 * override; let my WebView load the page return false; } 119 * // Otherwise, the link is not for a 120 * page on my site, so launch another Activity that handles URLs Intent intent = new 121 * Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent); 122 */ 123 if (!HTTP.equals(Uri.parse(url).getScheme())) { 124 String source = null; 125 try { 126 source = FileUtils.readToString(new File(Uri.parse(url).getPath())); 127 } catch (IOException e) { 128 throw new RuntimeException(e); 129 } 130 source = 131 "<script>" + mJsonSource + "</script>" + "<script>" + mAndroidJsSource + "</script>" 132 + "<script>" + mAPIWrapperSource + "</script>" + source; 133 mView.loadDataWithBaseURL(BASE_URL, source, "text/html", "utf-8", null); 134 } else { 135 mView.loadUrl(url); 136 } 137 return true; 138 } 139 } 140 141 @Override 142 public void onCreate() { 143 mView = new WebView(getActivity()); 144 mView.setId(1); 145 mView.getSettings().setJavaScriptEnabled(true); 146 mView.addJavascriptInterface(mWrapper, "_rpc_wrapper"); 147 mView.addJavascriptInterface(new Object() { 148 149 @SuppressWarnings("unused") 150 public void register(String event, int id) { 151 mObserver.register(event, id); 152 } 153 }, "_callback_wrapper"); 154 155 getActivity().setContentView(mView); 156 mView.setOnCreateContextMenuListener(getActivity()); 157 mChromeClient = new ChromeClient(getActivity()); 158 mWebViewClient = new MyWebViewClient(); 159 mView.setWebChromeClient(mChromeClient); 160 mView.setWebViewClient(mWebViewClient); 161 mView.loadUrl("javascript:" + mJsonSource); 162 mView.loadUrl("javascript:" + mAndroidJsSource); 163 mView.loadUrl("javascript:" + mAPIWrapperSource); 164 load(); 165 } 166 167 private void load() { 168 if (!HTTP.equals(Uri.parse(mUrl).getScheme())) { 169 String source = null; 170 try { 171 source = FileUtils.readToString(new File(Uri.parse(mUrl).getPath())); 172 } catch (IOException e) { 173 throw new RuntimeException(e); 174 } 175 mView.loadDataWithBaseURL(BASE_URL, source, "text/html", "utf-8", null); 176 } else { 177 mView.loadUrl(mUrl); 178 } 179 } 180 181 @Override 182 public void onDestroy() { 183 mReceiverManager.getReceiver(EventFacade.class).removeEventObserver(mObserver); 184 if (mDestroyManager) { 185 mReceiverManager.shutdown(); 186 } 187 mView.destroy(); 188 mView = null; 189 reference = null; 190 setResult(null); 191 } 192 193 public static void shutdown() { 194 if (HtmlActivityTask.reference != null) { 195 HtmlActivityTask.reference.finish(); 196 } 197 } 198 199 @Override 200 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 201 mUiFacade.onCreateContextMenu(menu, v, menuInfo); 202 } 203 204 @Override 205 public boolean onPrepareOptionsMenu(Menu menu) { 206 return mUiFacade.onPrepareOptionsMenu(menu); 207 } 208 209 private String generateAPIWrapper() { 210 StringBuilder wrapper = new StringBuilder(); 211 for (Class<? extends RpcReceiver> clazz : mReceiverManager.getRpcReceiverClasses()) { 212 for (MethodDescriptor rpc : MethodDescriptor.collectFrom(clazz)) { 213 wrapper.append(String.format(ANDROID_PROTOTYPE_JS, rpc.getName())); 214 } 215 } 216 return wrapper.toString(); 217 } 218 219 private class JavaScriptWrapper { 220 @SuppressWarnings("unused") 221 public String call(String data) throws JSONException { 222 Log.v("Received: " + data); 223 JSONObject request = new JSONObject(data); 224 int id = request.getInt("id"); 225 String method = request.getString("method"); 226 JSONArray params = request.getJSONArray("params"); 227 MethodDescriptor rpc = mReceiverManager.getMethodDescriptor(method); 228 if (rpc == null) { 229 return JsonRpcResult.error(id, new RpcError("Unknown RPC.")).toString(); 230 } 231 try { 232 return JsonRpcResult.result(id, rpc.invoke(mReceiverManager, params)).toString(); 233 } catch (Throwable t) { 234 Log.e("Invocation error.", t); 235 return JsonRpcResult.error(id, t).toString(); 236 } 237 } 238 239 @SuppressWarnings("unused") 240 public void dismiss() { 241 Activity parent = getActivity(); 242 parent.finish(); 243 } 244 } 245 246 private class HtmlEventObserver implements EventObserver { 247 private Map<String, Set<Integer>> mEventMap = new HashMap<String, Set<Integer>>(); 248 249 public void register(String eventName, Integer id) { 250 if (mEventMap.containsKey(eventName)) { 251 mEventMap.get(eventName).add(id); 252 } else { 253 Set<Integer> idSet = new HashSet<Integer>(); 254 idSet.add(id); 255 mEventMap.put(eventName, idSet); 256 } 257 } 258 259 @Override 260 public void onEventReceived(Event event) { 261 final JSONObject json = new JSONObject(); 262 try { 263 json.put("data", JsonBuilder.build(event.getData())); 264 } catch (JSONException e) { 265 Log.e(e); 266 } 267 if (mEventMap.containsKey(event.getName())) { 268 for (final Integer id : mEventMap.get(event.getName())) { 269 getActivity().runOnUiThread(new Runnable() { 270 @Override 271 public void run() { 272 mView.loadUrl(String.format("javascript:droid._callback(%d, %s);", id, json)); 273 } 274 }); 275 } 276 } 277 } 278 279 @SuppressWarnings("unused") 280 public void dismiss() { 281 Activity parent = getActivity(); 282 parent.finish(); 283 } 284 } 285 286 private class ChromeClient extends WebChromeClient { 287 private final static String JS_TITLE = "JavaScript Dialog"; 288 289 private final Activity mActivity; 290 private final Resources mResources; 291 private final ExecutorService mmExecutor; 292 293 public ChromeClient(Activity activity) { 294 mActivity = activity; 295 mResources = mActivity.getResources(); 296 mmExecutor = new SingleThreadExecutor(); 297 } 298 299 @Override 300 public void onReceivedTitle(WebView view, String title) { 301 mActivity.setTitle(title); 302 } 303 304 @Override 305 public void onReceivedIcon(WebView view, Bitmap icon) { 306 mActivity.getWindow().requestFeature(Window.FEATURE_RIGHT_ICON); 307 mActivity.getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, 308 new BitmapDrawable(mActivity.getResources(), icon)); 309 } 310 311 @Override 312 public boolean onJsAlert(WebView view, String url, String message, final JsResult result) { 313 final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class); 314 uiFacade.dialogCreateAlert(JS_TITLE, message); 315 uiFacade.dialogSetPositiveButtonText(mResources.getString(android.R.string.ok)); 316 317 mmExecutor.execute(new Runnable() { 318 319 @Override 320 public void run() { 321 try { 322 uiFacade.dialogShow(); 323 } catch (InterruptedException e) { 324 throw new RuntimeException(e); 325 } 326 uiFacade.dialogGetResponse(); 327 result.confirm(); 328 } 329 }); 330 return true; 331 } 332 333 @SuppressWarnings("unchecked") 334 @Override 335 public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) { 336 final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class); 337 uiFacade.dialogCreateAlert(JS_TITLE, message); 338 uiFacade.dialogSetPositiveButtonText(mResources.getString(android.R.string.ok)); 339 uiFacade.dialogSetNegativeButtonText(mResources.getString(android.R.string.cancel)); 340 341 mmExecutor.execute(new Runnable() { 342 343 @Override 344 public void run() { 345 try { 346 uiFacade.dialogShow(); 347 } catch (InterruptedException e) { 348 throw new RuntimeException(e); 349 } 350 Map<String, Object> mResultMap = (Map<String, Object>) uiFacade.dialogGetResponse(); 351 if ("positive".equals(mResultMap.get("which"))) { 352 result.confirm(); 353 } else { 354 result.cancel(); 355 } 356 } 357 }); 358 359 return true; 360 } 361 362 @Override 363 public boolean onJsPrompt(WebView view, String url, final String message, 364 final String defaultValue, final JsPromptResult result) { 365 final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class); 366 mmExecutor.execute(new Runnable() { 367 @Override 368 public void run() { 369 String value = null; 370 try { 371 value = uiFacade.dialogGetInput(JS_TITLE, message, defaultValue); 372 } catch (InterruptedException e) { 373 throw new RuntimeException(e); 374 } 375 if (value != null) { 376 result.confirm(value); 377 } else { 378 result.cancel(); 379 } 380 } 381 }); 382 return true; 383 } 384 } 385 } 386